Qué lenguaje escoger: Java o Kotlin

En primer lugar agradecerte que hayas escogido el curso “Android: Introducción a la programación”. Esperamos que toda la documentación que hemos preparado te sea de utilidad y en unas pocas semanas seas capaz de desarrollar tus propias aplicaciones para Android.

Kotlin ha sido nombrado, junto con Java, lenguaje oficial para el desarrollo en Android. Todo parece indicar que Google va a mantener ambos lenguajes.

Este curso ha sido preparado para poder seguirse en cualquiera de los dos lenguajes. La decisión va a ser tuya. Si no tienes muy claro por cual decidirte, en este artículo se exponen algunos argumentos para saques tus conclusiones. Empezamos analizando las ventajas de cada uno: 

Ventajas en Java

  •   Java es más sencillo y rápido de aprender que Kotlin. El requisito fundamental cando se diseñó Java era que fuera lo más simple posible.
  •   Java es el lenguaje de programación más popular del mundo. Aprender Java te abrirá muchas puertas en el mundo laboral.
  •   La mayoría de los ejemplos, o consultas en StackOverflow están en Java. Mucha de la documentación oficial de Google está disponible solo en Java.
 

Ventajas de Kotlin

  •   Es conciso y expresivo: vas a tener que escribir mucho menos para hacer lo mismo. Además, el código resulta mucho más fácil de entender.
  •   Es un lenguaje moderno: incorpora novedades como acceso directo a propiedades, lambdas, funciones de extensión, clases de datos, funciones inline, soporte nativo de delegados, y un largo etcétera.
Para finalizar un par de consejos:

Si eres un programador novato: Te recomiendo que comiences por Java. Cuando lo domines puedes plantearte pasarte a Kotlin. No te preocupes que no habrá sido tiempo perdido.

Si eres un programador experimentado o ya conoces Java: Te recomiendo que te enfrentes a este nuevo lenguaje. Tiene una curva de aprendizaje rápida y ya verás que acaba enganchando.

Estrategias localización v2

Estrategias para escoger un proveedor de localización

Determinar cuál es el proveedor de localización idóneo para nuestra aplicación puede resultar una tarea compleja. Además, esta decisión puede variar con el tiempo según el usuario cambie de posición, o puede desactivarse alguno de los proveedores. A continuación se plantean tres posibles estrategias.

Usar siempre el mismo tipo de proveedor

Los dos proveedores de localización disponibles en Android tienen características muy diferentes. Muchas aplicaciones tienen algún tipo de requisito que hace que podamos decantarnos de entrada por un sistema en concreto. Veamos algunos ejemplos.

Usaremos GPS si:

·         La aplicación requiere una precisión inferior a 10 m.

·         Está pensada para su uso al aire libre (p. ej., senderismo).

Usaremos localización por redes si:

·         El consumo de batería es un problema.

·         Está pensada para su uso en el interior de edificios (visita museo).

Una vez decidido, usaremos las constantes GPS_PROVIDER o NETWORK_PROVIDER de la clase LocationManager para indicar el proveedor deseado.

Existe un tercer tipo de proveedor identificado con la constante PASSIVE _PROVIDER. Puedes usarlo si quieres observar pasivamente actualizaciones de ubicación provocadas por otras aplicaciones, pero no quieres que se lancen nuevas lecturas de posición. De esta manera no provocamos consumo de energía adicional.

El mejor proveedor según un determinado criterio

Como vimos en el apartado anterior, la API de localización de Android nos proporciona la clase Criteria para seleccionar un proveedor de localización según el criterio indicado. Recordemos el código utilizado:

Criteria criterio = new Criteria();

criterio.setCostAllowed(false);

criterio.setAltitudeRequired(false);

criterio.setAccuracy(Criteria.ACCURACY_FINE);

proveedor = manejador.getBestProvider(criterio, true);

Los proveedores pueden variar de estado, por lo que podría ser interesante consultar cuál es el mejor proveedor cada vez que cambie su estado.

Usar los dos proveedores en paralelo

Otra alternativa podría ser programar actualizaciones de los dos proveedores de localización disponibles. Luego podríamos seleccionar la mejor localización entre las suministradas. Para estudiar esta alternativa realiza el siguiente ejercicio:

https://encrypted-tbn1.gstatic.com/images?q=tbn:ANd9GcRzLxOg2I343bfrEax5YX8gX5FTGpThiGtEf0FJTkM2BCEJVADlPg Vídeo[tutorial]: Estrategias de localizacion en Android

Ejercicio: Añadiendo localización en Mis Lugares

 

1.     Añade en AndroidManifest.xml de Mis Lugares el siguiente permiso:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

2.     Añade a la clase MainActivity las siguientes variables:

private LocationManager manejador;

private Location mejorLocaliz;

La variable mejorLocaliz almacenará la mejor localización actual.

3.     En el método onCreate() añade:

manejador = (LocationManager) getSystemService(LOCATION_SERVICE);

if (manejador.isProviderEnabled(LocationManager.GPS_PROVIDER)) {

   actualizaMejorLocaliz(manejador.getLastKnownLocation(
                                 LocationManager.GPS_PROVIDER));

}

if (manejador.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {

   actualizaMejorLocaliz(manejador.getLastKnownLocation(
                                 LocationManager.NETWORK_PROVIDER));

}

Comenzamos inicializando un LocationManager y tratando de buscar una primera localización para la variable mejorLocaliz. Usamos el método getLastKnownLocation() aplicado a los dos proveedores que vamos a utilizar. El método actualizaMejorLocaliz() se explicará más adelante.

4.     Añade a la clase los siguientes métodos:

@Override protected void onResume() {

   super.onResume();

   activarProveedores();

}

private void activarProveedores() {

   if (manejador.isProviderEnabled(LocationManager.GPS_PROVIDER)) {

      manejador.requestLocationUpdates(LocationManager.GPS_PROVIDER,

               20 * 1000, 5, this);

   }

   if (manejador.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {

      manejador.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,

               10 * 1000, 10, this);

   }

}

@Override protected void onPause() {

   super.onPause();

   manejador.removeUpdates(this);

}

Cuando la actividad pasa a activa, se llamará al método activarProveedores(), donde se programarán actualizaciones de los proveedores disponibles. Las actualizaciones se programan con un periodo de 20 y 10 segundos y con una distancia mínima de 5 o 10 m, dependiendo del proveedor. Para los dos proveedores, será nuestra clase quien actúe como escuchador. Cuando la actividad deje de estar activa eliminamos las actualizaciones.

5.     Nuestra clase ha de implementar la interfaz LocationListener:

public class MainActivity extends ListActivity implements LocationListener {

6.     Añade los métodos de esta interfaz, con el siguiente código:

@Override public void onLocationChanged(Location location) {

   Log.d(Lugares.TAG, "Nueva localización: "+location);

   actualizaMejorLocaliz(location);

}

@Override public void onProviderDisabled(String proveedor) {

   Log.d(Lugares.TAG, "Se deshabilita: "+proveedor);

   activarProveedores();

}

@Override   public void onProviderEnabled(String proveedor) {

   Log.d(Lugares.TAG, "Se habilita: "+proveedor);

   activarProveedores();

}

@Override

public void onStatusChanged(String proveedor, int estado, Bundle extras) {

   Log.d(Lugares.TAG, "Cambia estado: "+proveedor);

   activarProveedores();

}

Las acciones a realizar resultan evidentes: cuando la actualizamos cambie la posición y cuando cambie el estado tratamos de activar nuevos proveedores.

7.     En esta clase ya solo nos queda añadir el siguiente método:

private static final long DOS_MINUTOS = 2 * 60 * 1000;

 

private void actualizaMejorLocaliz(Location localiz) {

   if (localiz != null && (mejorLocaliz == null

         || localiz.getAccuracy() < 2*mejorLocaliz.getAccuracy()

         || localiz.getTime() - mejorLocaliz.getTime() > DOS_MINUTOS)) {

      Log.d(Lugares.TAG, "Nueva mejor localización");

      mejorLocaliz = localiz;

      Lugares.posicionActual.setLatitud(localiz.getLatitude());

      Lugares.posicionActual.setLongitud(localiz.getLongitude());

   }

}

En la variable mejorLocaliz almacenamos la mejor localización. Esta solo será actualizada con la nueva propuesta si: todavía no ha sido inicializada; o la nueva localización tiene una precisión aceptable (almenos la mitad que la actual); o la diferencia de tiempo es superior a dos minutos. Una vez comprobado si se cumple alguna de las tres condiciones, actualizamos mejorLocaliz y guardamos la posición en Lugares.posicionActual.

8.     Pasemos a declarar la constante TAG y la variable posicionActual. En la clase Lugares añade los atributos:

final static String TAG = "MisLugares";

protected static GeoPunto posicionActual = new GeoPunto(0,0);

9.     Una vez que ya disponemos de la posición actual, vamos a tratar de mostrar la distancia a cada lugar en el RecyclerView de la actividad principal. Abre el layout elemento_lista.xml y añade al final del RelativeLayout la nueva vista que se indica:

<TextView android:id="@+id/distancia"

          android:layout_width="wrap_content"

          android:layout_height="wrap_content"

          android:layout_alignParentBottom="true"

          android:layout_alignParentRight="true"

          android:layout_below="@id/direccion"

          android:singleLine="true"

          android:text="... Km" />

</RelativeLayout>

10.  En AdaptadorLugares, dentro de la clase ViewHolder, añade la variable:

public TextView distancia;

11.  En el constructor de ViewHolder añade al final:

distancia = (TextView) vistaReciclada.findViewById(R.id.distancia);

12.  Dentro del método personalizaVista() añade el siguiente código al final:

if (Lugares.posicionActual != null && lugar.getPosicion() != null) {

   int d=(int) Lugares.posicionActual.distancia(lugar.getPosicion());

   if (d < 2000) {

      holder.distancia.setText(d + " m");

   } else {

      holder.distancia.setText(d / 1000 + " Km");

   }

}

Nos aseguramos de que la posición actual y la del lugar existen. Luego calculamos la distancia en la variable d. Si la distancia es inferior a 2000, se muestra en metros; en caso contrario se muestra en Km.

13.  Ejecuta la aplicación y verifica el resultado obtenido:

 

 

La vista RecyclerView

La vista RecyclerView visualiza una lista o cuadrícula deslizable de varios elementos, donde cada elemento puede definirse mediante un layout. Su utilización es algo compleja, pero muy potente. Un ejemplo lo podemos ver en la siguiente figura:
 

Dentro del API de Android encontramos las vistas ListView y GridView nos ofrece una alternativa a RecyclerView. Esta última no ha sido añadida a ningún API si no que se añade en una librería de compatibilidad. A pesar de que resulta algo más compleja de manejar, recomendamos el uso de RecyclerView, en lugar de ListView o GridView, al ser más eficiente y flexible. Aunque su uso se describe con detalle en El Gran Libro de Android Avanzado, hacemos en este punto una introducción de sus funcionalidades básicas. Las principales ventajas que ofrece RecyclerView frente a ListView o GridView son:

  • Reciclado de vistas (RecyclerView.ViewHolder)
  • Distribución de vistas configurable (LayoutManager)
  • Animaciones automáticas (ItemAnimator)
  • Separadores de elementos (ItemDecoration)
  • Trabaja conjuntamente con otros witgets introducidos en Material Design (CoordinationLayout)

video[Tutorial] Creación de listas con RecyclerView.

Crear una lista (o cuadrícula) de elementos con un RecyclerView conlleva los siguientes pasos:

  • Diseñar un Layout que contiene el RecyclerView.
  • Implementar la actividad que lo visualice el RecyclerView
  • Diseñar un Layout individual que se repetirá en la lista
  • Personalizar cada una de los Layouts individuales según nuestros datos utilizando un adaptador.
  • Definir como queremos que se posicionen los elementos en las vistas. Por ejemplo en forma de lista o de cuadricula.

Los tres primeros pasos anteriores son similares al uso de cualquier otro tipo de vista. Los dos últimos sí que requieren una explicación más extensa:

Personalizar los datos a mostrar

Para personalizar los elementos a mostrar en un RecyclerView hemos de usar un adaptador. La creación de adaptadores puede ser delicada, en algunos casos podemos tener problemas de eficiencia. Para evitar estos problemas, Google ha cambiado la forma de trabajar con RecyclerView. Ya no se puede utilizar la interfaz Adapter, si no que se ha de utilizar la clase RecyclerView.Adapter.

video[Tutorial] El patrón ViewHolder y su uso en un RecyclerView.

Distribuir los elementos

A diferencia ListView o GridView, que muestran los elementos usando una determinada configuración,  RecyclerView puede configurar esta distribución por medio de la clase LayoutManager. El sistema nos proporciona tres descendientes de LayoutManager, que son mostrados en la siguiente figura. También podemos crear nuestro descendiente de LayoutManager.

                             

En los siguientes ejercicios usaremos un RecyclerView en Mis Lugares. La actividad inicial de la aplicación nos permite escoger entre cuatro botones. Sin embargo, sería mucho más interesante que en esta actividad se visualizara directamente una lista con los lugares.

Ejercicio: Un RecyclerView en Mis Lugares

1. Añade al fichero Gradle Scripts/Bulid.gradle (Module:app) la siguiente dependencia

dependencies {
    …
    implementation 'androidx.recyclerview:recyclerview:1.2.1'
}

NOTA: Si lo deseas, puedes saltarte este paso. Más adelante, cuando aparezca RecyclerView en el código Java, la clase aparecerá marcada en rojo, al no encontrar su declaración. Al pulsar sobre la clase, aparecerá una bombilla roja donde podrás elegir la opción Add dependency on androidx.recyclerview:recyclerview. El mismo añadirá la dependencia, con la ventaja de seleccionar la última versión disponible.

La clase ReciclerView no ha sido añadida al API de Android, si no que se encuentra en una librería externa. Esto tiene la gran ventaja de que aunque esta clase aparece en la versión 5 de Android, puede ser usada en versiones anteriores. Realmente, solo puede ser utilizada a partir del nivel de API 7. Pero el 100 % de los dispositivos que se comercializan en la actualidad cumplen este requisito.

2. Reemplaza en el layout content_main.xml  por el  siguiente código

<androidx.recyclerview.widget.RecyclerView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/recycler_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior" />

3.  En la práctica “Recursos alternativos en Mis Lugares” se crea un recurso alternativo para este layout en res/layout-land/content_main.xml. Elimina este recurso alternativo.
NOTA: Al borrarlo has de desactivar la opción Safe delete.
Elimina también el resto de recursos alternativos para content_main.xml.

4.  El layout content_main se encuentra incrustado dentro activity_main por medio de un include. Para poder acceder a las vistas del layout incrustado usando View Binding, es necesario que el include tenga un id. Añade el código subrayado en el layout activity_main:
 

android:id="@+id/content" layout="@layout/content_main" />
5.  En la actividad MainActivity añade el código subrayado:

public class MainActivity extends AppCompatActivity {
   …
   public AdaptadorLugares adaptador;

   @Override 
   protected void onCreate(Bundle savedInstanceState) {
   	 
     …
      adaptador = ((Aplicacion) getApplication()).adaptador; 
      RecyclerView recyclerView = binding.content.recyclerView;
      recyclerView.setHasFixedSize(true);
      recyclerView.setLayoutManager(new LinearLayoutManager(this));
      recyclerView.setAdapter(adaptador);
   }
   … 
class MainActivity : AppCompatActivity() {
   …
   lateinit var adaptador: AdaptadorLugares
 
   override fun onCreate(savedInstanceState: Bundle?) {
      …
      usoLugar = CasosUsoLugar(this, lugares, adaptador)
      adaptador = (application as Aplicacion).adaptador
      binding.content.recyclerView.apply {
         setHasFixedSize(true)
         layoutManager = LinearLayoutManager(this@MainActivity)
         adapter = adaptador
      }
   }
   … 

Declaramos recyclerView y lo inicializamos con binding.content.recyclerView. Recuerda que content es el id del layout incrustado. Declaramos un adaptador y lo inicializamos desde la clase Aplicación.. La clase AdaptadorLugar será definida a continuación. Además, indicamos que las vistas a mostrar serán de tamaño fijo y que usaremos un LayoutManager de tipo LinearLayoutManager. Finalmente, asignamos el adaptador al RecyclerView.

6.  De ser necesario, elimina del método onCreate() el código destinado a inicializar los botones. Los botones van a ser reemplazados por el RecyclerView.

7.   En la clase Aplicacion crea la variable adaptador:

public AdaptadorLugares adaptador = new AdaptadorLugares(lugares);
val adaptador = AdaptadorLugares(lugares)

8.  Ahora hemos de definir el layout que representará cada uno de los elementos de la lista. Crea el fichero res/layout/elemento_lista.xml con el siguiente código:

<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="4dp">
   <ImageView android:id="@+id/foto"
           android:layout_width="?android:attr/listPreferredItemHeight"
           android:layout_height="?android:attr/listPreferredItemHeight"
           android:contentDescription="fotografía"
           android:src="@drawable/bar"
           app:layout_constraintTop_toTopOf="parent"
           app:layout_constraintLeft_toLeftOf="parent"/>
   <TextView android:id="@+id/nombre"
           android:layout_width="0dp"
           android:layout_height="wrap_content"
           android:text="Nombres del lugar"
           android:textAppearance="?android:attr/textAppearanceMedium"
           android:textStyle="bold"
           android:maxLines="1"
           app:layout_constraintTop_toTopOf="parent"
           app:layout_constraintStart_toEndOf="@+id/foto"
           app:layout_constraintEnd_toEndOf="parent"/>
   <TextView android:id="@+id/direccion"
           android:layout_width="0dp"
           android:layout_height="wrap_content"
           android:gravity="center"
           android:maxLines="1"
           android:text="dirección del lugar"
           app:layout_constraintTop_toBottomOf="@id/nombre"
           app:layout_constraintStart_toEndOf="@+id/foto"
           app:layout_constraintEnd_toEndOf="parent"/>
   <RatingBar android:id="@+id/valoracion"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           style="?android:attr/ratingBarStyleSmall"
           android:isIndicator="true"
           android:rating="3"
           app:layout_constraintTop_toBottomOf="@id/direccion"
           app:layout_constraintLeft_toRightOf="@+id/foto"
           app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout> 

Para combinar las vistas se ha escogido un ConstraintLayout. El primer elemento que contiene es un ImageView alineado a la izquierda. Su altura se establece a partir de un parámetro de configuración del sistema ?android:attr/listPreferredItemHeight (altura preferida para ítem de lista). Su altura es la misma, por lo tanto, la imagen será cuadrada. A la derecha se muestran dos textos. En el texto de mayor tamaño se visualizará el nombre del lugar y en el de menor tamaño, la dirección. Bajo estos textos se ha incluido un RatingBar.

9.  El siguiente paso será crear la clase AdaptadorLugares, que se encargará de rellenar el ReciclweView.

public class AdaptadorLugares extends 
                      RecyclerView.Adapter<AdaptadorLugares.ViewHolder> {
    protected RepositorioLugares lugares;         // Lista de lugares a mostrar
    public AdaptadorLugares(RepositorioLugares lugares) {
        this.lugares = lugares;
    }

    //Creamos nuestro ViewHolder, con los tipos de elementos a modificar
    public static class ViewHolder extends RecyclerView.ViewHolder {
       public TextView nombre, direccion;
       public ImageView foto;
       public RatingBar valoracion;
       public ViewHolder(ElementoListaBinding itemView) {
          super(itemView.getRoot());
          nombre = itemView.findViewById(R.id.nombre);
          direccion = itemView.findViewById(R.id.direccion);
          foto = itemView.findViewById(R.id.foto);
          valoracion= itemView.findViewById(R.id.valoracion);
       }
       // Personalizamos un ViewHolder a partir de un lugar
       public void personaliza(Lugar lugar) {
          nombre.setText(lugar.getNombre());
          direccion.setText(lugar.getDireccion());
          int id = R.drawable.otros;
          switch(lugar.getTipo()) {
             case RESTAURANTE:id = R.drawable.restaurante; break;
             case BAR:    id = R.drawable.bar;     break;
             case COPAS:   id = R.drawable.copas;    break;
             case ESPECTACULO:id = R.drawable.espectaculos; break;
             case HOTEL:   id = R.drawable.hotel;    break;
             case COMPRAS:  id = R.drawable.compras;   break;
             case EDUCACION: id = R.drawable.educacion;  break;
             case DEPORTE:  id = R.drawable.deporte;   break;
             case NATURALEZA: id = R.drawable.naturaleza; break;
             case GASOLINERA: id = R.drawable.gasolinera; break;  }
          foto.setImageResource(id);
          foto.setScaleType(ImageView.ScaleType.FIT_END);
          valoracion.setRating(lugar.getValoracion());
       }
    }

    // Creamos el ViewHolder con la vista de un elemento sin personalizar
    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // Inflamos la vista desde el xml
        ElementoListaBinding v = ElementoListaBinding.inflate(LayoutInflater
                               .from(parent.getContext()), parent, false);
      return new AdaptadorLugares.ViewHolder(v);
    }

    // Usando como base el ViewHolder y lo personalizamos
    @Override
    public void onBindViewHolder(ViewHolder holder, int posicion) {
        Lugar lugar = lugares.elemento(posicion);
        holder.personaliza(lugar);
    }
    // Indicamos el número de elementos de la lista
    @Override public int getItemCount() {
        return lugares.tamaño();
    }
} 
class AdaptadorLugares(private val lugares: RepositorioLugares) :
                     RecyclerView.Adapter<AdaptadorLugares.ViewHolder>() {

   class ViewHolder(val view: ElementoListaBinding) : 
                                     RecyclerView.ViewHolder(view.root) {

      fun personaliza(lugar: Lugar) = with(View){
         nombre.text = lugar.nombre
         direccion.text = lugar.direccion
         foto.setImageResource(when (lugar.tipoLugar) {
            TipoLugar.RESTAURANTE -> R.drawable.restaurante
            TipoLugar.BAR -> R.drawable.bar
            TipoLugar.COPAS -> R.drawable.copas
            TipoLugar.ESPECTACULO -> R.drawable.espectaculos
            TipoLugar.HOTEL -> R.drawable.hotel
            TipoLugar.COMPRAS -> R.drawable.compras
            TipoLugar.EDUCACION -> R.drawable.educacion
            TipoLugar.DEPORTE -> R.drawable.deporte
            TipoLugar.NATURALEZA -> R.drawable.naturaleza
            TipoLugar.GASOLINERA -> R.drawable.gasolinera
            TipoLugar.OTROS -> R.drawable.otros
         })
         foto.setScaleType(ImageView.ScaleType.FIT_END)
         valoracion.rating = lugar.valoracion
      }
   }

   override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
                                                             ViewHolder {
      val binding = ElementoListaBinding.inflate(
                      LayoutInflater.from(parent.context),  parent, false)
      return ViewHolder(binding)
   }

   override fun onBindViewHolder(holder: ViewHolder, posicion: Int) {
      val lugar = lugares.elemento(posicion)
      holder.personaliza(lugar)
   }

   override fun getItemCount() = lugares.tamaño()
} 

Un adaptador es un mecanismo estándar en Android que nos permite crear una serie de vistas que han de ser mostradas dentro de un contenedor. Con las vistas ListView, GridView, Spiner o Gallery has de crear el adaptador utilizando la interfaz Adapter.  Pero con RecyclerView  has de utilizar la clase RecyclerView.Adapter.

En el constructor se inicializa el conjunto de datos a mostrar (en el ejemplo lugares) y otras variables globales a la clase. El objeto inflador nos va a permitir crear una vista a partir de su XML.

Luego se crea la clase ViewHolder, que contendrá las vistas que queremos modificar de un elemento (en concreto: dos TextView con el nombre y la dirección, un ImageView con la imagen del tipo de lugar y un RatingBar). Esta clase es utilizada para evitar tener que crear las vistas de cada elemento desde cero. Lo va a hacer es utilizar un ViewHolder que contendrá las cuatro vistas ya creadas, pero sin personalizar. De forma que, gastará el mismo ViewHolder para todos los elementos y simplemente lo personalizaremos según la posición. Es decir, reciclamos el ViewHolder. Esta forma de proceder mejora el rendimiento del ReciclerView, haciendo que funcione más rápido.

El método onCreateViewHolder() devuelve una vista de un elemento sin personalizar. Podríamos definir diferentes vistas para diferentes tipos de elementos utilizando el parámetro viewType.Usamos el método inflate() para crear una vista a partir del layout XML definido en elemento_lista. En este método se indica como segundo parámetro el layout padre que contendrá a la vista que se va a crear. En este caso, resulta imprescindible indicarlo, ya que queremos que la vista hijo ha de adaptarse al tamaño del padre (en elemento_lista se ha indicado layout_width="match_parent").  El tercer parámetro del método permite indicar si queremos que la vista sea insertada en el padre. Indicamos false, dado que esta operación la va a hacer el ReciclerView.

El método onBindViewHolder() personaliza un elemento de tipo ViewHolder según su posicion. A partir del ViewHolder que personalizamos ya es el sistema quien se encarga de crear la vista definitiva que será insertada en el ReciclerView. Finalmente, el método getItemCount() se utiliza para indicar el número de elementos a visualizar.

10.       Ejecuta la aplicación y verifica el resultado.

Ejercicio: Selección de un elemento en un RecyclerView

En Queremos que cuando se seleccione un elemento de la actividad principal de Mis Lugares, se abra la actividad que visualiza su contenido. Va a ser similar al ejercicio Selección de un elemento en un RecyclerView que vimos para Asteroides. La mayor diferencia es que ahora usaremos View Binding.
 

1. Añade a la clase AdaptadorLugares la siguiente declaración:

protected View.OnClickListener onClickListener;
   	  
lateinit var onClick: (View) -> Unit  

2.Para poder modificar el campo anterior añade el siguiente setter, solo en Java:

public void setOnItemClickListener(View.OnClickListener onClickListener) {
    this.onClickListener = onClickListener;
}

3.    Solo nos queda aplicar este escuchador a cada una de las vistas creadas. Añade la línea subrayada en el método onCreateViewHolder():

@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ElementoListaBinding v = ElementoListaBinding.inflate(LayoutInflater
                               .from(parent.getContext()), parent, false);
    v.getRoot().setOnClickListener(onClickListener);
    return new AdaptadorLugares.ViewHolder(v);
}
fun personaliza(lugar: Lugar, onClick: (View) -> Unit) = with(itemView) {
   …
   setOnClickListener{ onClick(itemView) }
}
…
override fun onBindViewHolder(holder: ViewHolder, posicion: Int) {
    …
    holder.personaliza(lugar, onClick)
}

4.    Desde la clase MainActivity vamos a asignar un escuchador. Para ello añade el siguiente código al final del método onCreate():

adaptador.setOnItemClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        int pos= binding.content.recyclerView.getChildAdapterPosition(v);
        usoLugar.mostrar(pos);
    }
});
adaptador.onClick = {
   val pos: Int = binding.content.recyclerView.getChildAdapterPosition(it)
   usoLugar.mostrar(pos)
}

El método getChildAdapterPosition(), nos indicarán la posición de una vista dentro del adaptador.

5.    Ejecuta la aplicación y verifica el resultado.

ejerEjercicio: Refrescar el RecyclerView de Mis Lugares

Si borras o editas un lugar y regrsas al RecyclerView donde se muestran los lugares, comprovarás que en la lista no se han actualizado. Para resolverlo sigue los siguientes pasos:
1.  Añade en MainActivity el siguiente método:

@Override protected void onResume(){
   super.onResume();
   adaptador.notifyDataSetChanged();
}
override fun onResume() {
   super.onResume()
   adaptador.notifyDataSetChanged()
}

El método onResume() es ejecutado cada vez que nuestra actividad vuelve a pasar a primer plano. Pertenece a una de las funciones del ciclo de vida de una actividad, que será estudiado más adelante. En este método, tras llamar al super, le indicamos al adaptador que los datos pueden haber cambiado y que vuelva a generar cada una de las vistas que componen la lista. 
2.  Ejecuta la aplicación y verifica el resultado.

Preguntas de repaso: RecyclerView

La aplicación Mis Lugares

La aplicación Mis Lugares va a ser desarrollada a lo largo del curso MOOC "Android: Introducción a la programación". También puede desarrollarse siguiendo "El Gran Libro de Android" ed. Marcombo.

Mis Lugares es una aplicación para gestionar información personal. En concreto, podemos registrar los lugares que hayamos visitado recientemente. Se ha escogido este tipo de aplicación al cubrir aspectos fácilmente reutilizables. Entre otros:

  • Manipulación de diferentes tipos de información: direcciones, fotografías, fechas, geolocalizaciones, valoraciones, …
  • Interfaz de usuario en Android: vistas, layouts, RecyclerView, preferencias, barra de acciones,…
  • Geolocalización: basado en GPS y redes WiFi y telefónica
  • Uso de Google Maps
  • Almacenamiento en una bases de datos SQLite
  • Uso Fragments para una correcta visualización en tableta

Hemos actualizado el curso añadiendo interesantes novedades:

  • ConstraintLayout
  • Interfaz de usuario basado en Material Design
  • Definición de la paleta de colores de la aplicación
  • Permisos en Android 6
  • Barra de acciones con Toolbar
  • Vistas animadas: CoordinationLayout, AppBarLayout, FloatingActionButton y SnackBar

NOTA: El siguiente vídeo muestra la apariencia de la aplicación que se desarrollo la edición anterior. La que desarrollaremos en este curso está basada en Material Design y tiene un diseño más actual.

   video[Tutorial]  Descripción de la aplicación Mis Lugares

Los Fragments

Con la popularización de las tabletas surgió el problema de desarrollar simultáneamente una aplicación para ser ejecutada tanto en un móvil como en una tableta. En otros sistemas, como iOS, se decidió que el desarrollador tenía que implementar dos aplicaciones diferentes. Android siguió con la estrategia de usar recursos alternativos para adaptarse a los diferentes tamaños de pantalla. Las herramientas vistas hasta ahora no resultan suficientes: cuando se diseña una interfaz de usuario específica para una tableta, no solo es preciso adaptar el tamaño de letra o los márgenes, sino que también es necesario reestructurar cómo se muestra la información en pantalla. En una tableta pueden caber muchos elementos de diseño al mismo tiempo, mientras que en un móvil estamos más limitados. Por ejemplo, podríamos diseñar dos elementos de la interfaz de usuario: uno que nos permitiera elegir entre una lista de lugares y otro que mostrara los detalles de uno de esos lugares. En una tableta se podrían mostrar ambos elementos a la vez, mientras que en un móvil tendríamos que mostrar primero uno y luego el otro.

video[Tutorial] Los Fragments en Android.

Para resolver este problema, en la versión 3.0 de Android se introdujeron los fragments. Los fragments son bloques de interfaz de usuario que pueden utilizarse en diferentes sitios, simplificando así la composición de una interfaz de usuario. Los fragments nos permiten diseñar y crear cada uno de los elementos de nuestra aplicación por separado. Luego, dependiendo del tamaño de pantalla disponible, mostraremos uno solo o más de uno a la vez.

Figura 1. Uso de fragmentsen tableta y móvil.

Es importante resaltar que no cambia el papel de las actividades. Sigue siendo el elemento básico que representa cada pantalla de una aplicación y nos permite navegar por ella. La novedad introducida es que cuando diseñemos una actividad, esta puede estar formada por uno o más fragments.

Cuando diseñemos un fragment, este ha degestionarse a sí mismo, recibiendo eventos de entrada y modificando su vista sin necesidad de que la actividad que lo contiene intervenga. De esta forma, el fragment se podrá utilizar en diferentes actividades sin tener que modificar el código.

fragments. El problema es que esta característica aparece en una versión que todavía no está disponible en muchos dispositivos. Para resolver este problema se ha creado una librería de compatibilidad para poder utilizar fragments en versiones anteriores a la 3.0. Esta librería se incluye de manera automática a un proyecto, siempre que el requerimiento mínimo de SDK sea inferior al nivel 11 (3.0); pero lo desarrollemos con una versión superior a la 3.0 (Target SDK). Para verificar esto, abre el proyecto creado en el ejercicio anterior. Observa cómo esta librería se incluye en libs/android-support-v4.jar.

 Cada fragment ha de implementarse en una clase diferente. Esta clase tiene una estructura similar a la de una actividad, pero con algunas diferencias. La primera es que esta clase tiene que extender Fragment. El ciclo de vida es muy parecido al de una actividad; sin embargo, dispone de unos cuantos eventos más, que le indican cambios en su estado con respecto a la actividad que lo contiene. El ciclo de vida de un fragment va asociado al de la actividad que lo contiene (por ejemplo, si la actividad es destruida, todos los fragments que contiene son destruidos); pero también es posible destruir un fragment sin modificar el estado de la actividad.

Los fragments suelen mostrar una vista (aunque esto no es imprescindible). Es recomendable definir esta vista en un fichero XML de recursos. Por lo tanto, para crear un fragment usaremos una clase Java para definir su comportamiento y un fichero XML para definir su apariencia.

Los fragments se pueden introducir en una actividad de dos formas diferentes: por código o desde XML. Ambas formas tienen sus ventajas y sus inconvenientes. Introducir un fragment desde XML es más sencillo. Además, el diseño queda diferenciado del código, simplificando el trabajo del diseñador. Sin embargo, trabajar de esta forma tiene un inconveniente: una vez introducido ya no podremos reemplazar el fragment por otro. Por lo tanto, un fragment añadido desde XML será siempre estático. Si lo añadimos desde código, ganamos la posibilidad de intercambiar el fragment por otro. En los siguientes ejercicios veremos cómo añadir fragments desde XML.

Uso de Fragments en Mis Lugares

Ejercicio: Un primer fragment.
 

En este ejercicio modificaremos la aplicación Mis Lugares, pero ahora trabajando con un fragment. Su funcionalidad será idéntica. La ventaja de definir fragments en vez de actividades es que podemos mostrar varios fragments a la vez en la pantalla, pero no varias actividades.
 

1.    En este apartado vamos a realizar un número importante de modificaciones y es posible que algo salga mal. Puede ser un buen momento para realizar una copia del proyecto actual. Así, siempre dispondremos de una versión operativa. Para ello, desde el explorador de ficheros de tu sistema operativo, realiza una copia de la carpeta que contiene el proyecto. Para acceder rápidamente a esta carpeta desde el explorador del proyecto, pulsa en app con el botón derecho y selecciona Show in Explorer
 

2.    En este ejercicio vamos a mostrar en un fragment lo que antes se mostraba en MainActivity. Por lo tanto, podemos reutilizar su layout en XML para nuestro fragment. Copia el fichero content_main.xml en fragment_selector.xml. Desde el explorador del proyecto usa Ctrl-C y Ctrl-V.
 

3.    Ahora nos falta definir la clase para el fragment. Crea una nueva clase llamada SelectorFragment y rellénala con el siguiente código:
 

public class SelectorFragment extends Fragment {
   private LugaresBDAdapter lugares;
   private AdaptadorLugaresBD adaptador;
   private CasosUsoLugar usoLugar;
   private RecyclerView recyclerView;

   @Override
   public View onCreateView(LayoutInflater inflador, ViewGroup contenedor,
                            Bundle savedInstanceState) {
      View vista = inflador.inflate(R.layout.fragment_selector,
            contenedor, false);
      recyclerView = vista.findViewById(R.id.recyclerView);
      return vista;
   }

   @Override
   public void onActivityCreated(Bundle state) {
      super.onActivityCreated(state);
      lugares = ((Aplicacion) getActivity().getApplication()).lugares;
      adaptador = ((Aplicacion) getActivity().getApplication()).adaptador;
      usoLugar = new CasosUsoLugar(getActivity(), lugares);
      recyclerView.setHasFixedSize(true);
      recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
      recyclerView.setAdapter(adaptador);
      adaptador.setOnItemClickListener(new View.OnClickListener() {
         @Override public void onClick(View v) {
            int pos = (Integer)(v.getTag());
            usoLugar.mostrar(pos);
         }
      });
   }
} 
class SelectorFragment : Fragment() {
   val lugares by lazy { (activity!!.application as Aplicacion).lugares }
   val adaptador by lazy { (activity!!.application as Aplicacion).adaptador}
   lateinit var usoLugar: CasosUsoLugar
   lateinit var recyclerView: RecyclerView

   override fun onCreateView(inflador: LayoutInflater, contenedor: 
                        ViewGroup?, savedInstanceState: Bundle? ): View? {
      val vista =   
             inflador.inflate(R.layout.fragment_selector,contenedor,false)
      recyclerView = vista.findViewById(R.id.recyclerView)
      return vista
   }

   override fun onActivityCreated(state: Bundle?) {
      super.onActivityCreated(state)
      usoLugar = CasosUsoLugar(activity!!, lugares)
      recyclerView.apply {
         setHasFixedSize(true)
         layoutManager = LinearLayoutManager(context)
         adapter = adaptador
      }
      adaptador.onClick = {
         val pos = it.tag as Int
         usoLugar.mostrar(pos)
      }
   }
}  

NOTA: Tras incluir nuevas clases tendrás que indicar los importsadecuados. Para que Android Studio lo haga automáticamente pulsa Alt-Intro. La clase Fragment aparece en dos paquetes, por lo que te pedirá que selecciones uno de los dos. Utiliza el de la librería androidx.fragment.app.Fragment.


El código de esta clase es similar al que teníamos antes en MainActivity, salvo que ahora extendemos a Fragment en vez de a AppCompatActivity, y que los métodos del ciclo de vida son diferentes.

Al igual que en una actividad, un fragment también tiene una vista asociada. En la actividad asociábamos la vista en el método onCreate(), llamando a setContentView(). En un fragment también disponemos del método onCreate(), pero no es aquí donde hay que asociar la vista. Se ha creado un nuevo método en el ciclo de vida, onCreateView(), con la finalidad de asociar su vista. En este método se nos pasan tres parámetros: un LayoutInflater que nos permite crear una vista a partir de un layout XML, el contenedor donde será insertado el fragment (en el punto siguiente veremos que se trata de un LinearLayout)  y posibles valores guardados de una instancia anterior[1]. El método onCreateView() ha de devolver la vista ya creada. El hecho de disponer de este método va a resultar muy interesante, dado que nos va a permitir cambiar la vista de un fragment sin tener que volverlo a crear.

Por otra parte onActivityCreated() es llamado cuando la actividad que contiene el fragment termina de crearse. Aprovecharemos este método para realizar tareas de inicialización, como por ejemplo crear el adaptador y asociarlo al RecyclerView. Observa como la forma de trabajar con un RecyclerView es diferente cuando lo hacemos desde una actividad que extiende Fragment. Aunque, ha de quedar claro, que en el fondo se realiza la misma tarea. En el siguiente esquema se compara cómo asociar el layout, el RecyclerView  tanto en ua actividad como en un fragment.


 

4.    La actividad MainActivity va a visualizar el layout content_main.xml. Reemplaza su contenido por el siguiente código:

<?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"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:orientation="horizontal"
   app:layout_behavior="@string/appbar_scrolling_view_behavior">
   <fragment
      android:id="@+id/selector_fragment"
      android:name= "com.example.mislugares.presentacion.SelectorFragment"
      android:layout_width="0dp"
      android:layout_height="match_parent"
      android:layout_weight="1" />
</LinearLayout> 


Como puedes ver, introducir el fragment desde un XML es muy sencillo. Simplemente añadimos una etiqueta <fragment> y en el atributo name indicamos el nombre de la clase del fragment. Este fragment es introducido en un LinearLayoutque actuará de contenedor. Es habitual usar un contenedor para poder añadir nuevos fragments o reemplazarlos. Es importante incluir el atributo layout_behavior para su correcto funcionamiento dentro del CoordinatorLayout.

5.    Elimina en MainActivity la declaración de las variables globales  recyclerView.En el método onCreate()elimina las líneas que inicializan recyclerView y la asignación del evento onClick para adaptador.

6.    Ejecuta el proyecto. Verifica que la aplicación tiene la misma funcionalidad que antes.
 

NOTA: Puede parecer que no hemos conseguido gran cosa, dado que al final el funcionamiento es idéntico. Aunque resulta más complejo trabajar con fragments, Google recomienda que siempre diseñemos los elementos del IU basados en fragments, en lugar de en actividades. De esta forma, tendremos la posibilidad de mostrar varios elementos del IU a la vez en pantalla.

.

Ejercicio: Implementando un segundo fragment.
 

Recordemos que la aplicación que queremos hacer tiene que mostrar una serie de lugares, y que al pulsar sobre uno de ellos, nos muestre la información detallada sobre él. En este ejercicio crearemos un segundo fragment para mostrar la información de un lugar, utilizando como base la actividad VistaLugarActivity. Haremos también que MainActivity muestre simultáneamente los dos fragments que hemos creado.

1.     Desde el explorador del proyecto copia la actividad VistaLugarActivity (Ctrl-C) y realiza una copia (Ctrl-V) con nombre VistaLugarFragment.

2.     Haz que la nueva clase herede de Fragment en lugar de AppCompatActivity.

NOTA: Importa esta clase del paquete androidx.fragment.app.Fragment.

3.     Elimina el método onCreate() y distribuye su código entre los siguientes:

@Override public View onCreateView(LayoutInflater inflador, 
                        ViewGroup contenedor,Bundle savedInstanceState) {
   setHasOptionsMenu(true);
   View vista = inflador.inflate(R.layout.vista_lugar,contenedor,false);
   return vista;
}

@Override public void onActivityCreated(Bundle state) {
   super.onActivityCreated(state);
   lugares = ((Aplicacion) getActivity().getApplication()).lugares;
   adaptador = ((Aplicacion) getActivity().getApplication()).adaptador;
   usoLugar = new CasosUsoLugar(getActivity(), lugares);
   v = getView();
   foto = v.findViewById(R.id.foto);
   Bundle extras = getActivity().getIntent().getExtras();
   if (extras != null)
        pos = extras.getInt("pos", 0);
   else pos = 0;
   actualizaVistas();
}  
val lugares by lazy { (activity!!.application as Aplicacion).lugares }
val adaptador by lazy { (activity!!.application as Aplicacion).adaptador }
lateinit var usoLugar: CasosUsoLugar
…

override fun onCreateView(inflador: LayoutInflater, contenedor:ViewGroup?,
                          savedInstanceState: Bundle? ): View? {
   setHasOptionsMenu(true)
   val vista = inflador.inflate(R.layout.vista_lugar, contenedor, false)
   return vista
}

override fun onActivityCreated(state: Bundle?) {
   super.onActivityCreated(state)
   pos = activity?.intent?.extras?.getInt("pos", 0) ?: 0
   usoLugar = CasosUsoLugar(this activity!!, lugares)
   actualizaVistas()
} 

El layout que visualizará el fragment es el mismo que usábamos en la actividad pero ahora es asignado en el método onCreateView(). La recogida de parametros y la inicialización se realiza en onActivityCreated().

4.     Añade al principio de actualizaVistas():

lugar = adaptador.lugarPosicion(pos); 

Hemos hecho público este método para permitir que se llame desde fuera de la clase. Si el valor de pos ha cambiado tenemos que obtener el lugar adecuado.

5.     En Java, reemplaza todas las apariciones de findViewById() por v.findViewById(). Este método no está en la clase Fragment, pero sí que está en la clase View. Declara la variable global v de tipo View.

6.     En Java, cambia el modificador de onActivityResult() de protected a public. Para poder sobreescribir un método es imprescindible que uses los mismos modificadores, y para el método en cuestión son diferentes en la clase Activity que en Fragment.

7.     Reemplaza el método onCreateOptionsMenu() por el siguiente:

@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    inflater.inflate(R.menu.vista_lugar, menu);
    super.onCreateOptionsMenu(menu, inflater); 
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
   inflater.inflate(R.menu.vista_lugar, menu)
   super.onCreateOptionsMenu(menu, inflater)
} 

Desde un fragment también podemos añadir ítems de menú a la actividad. El procedimiento es muy parecido, solo cambia el perfil del método.
 

8.   Ya no estamos en una actividad has de reemplazar las apariciones de this por una referencia a la actividad del fragment:

Toast.makeText(this getActivity(), "… 
Toast.makeText(this activity, "… 

9.    Modifica content_main.xml para que muestre ambos fragments. Para ello añade el siguiente elemento al final del LinearLayout:

<fragment
     android:id="@+id/vista_lugar_fragment"
     android:name="com.example.mislugares.presentacion.VistaLugarFragment"
     android:layout_width="0dp"
     android:layout_height="match_parent"
     android:layout_weight="1" />

10.   Ejecuta la aplicación. Podrás ver como se muestran los dos fragments uno al lado del otro. De momento, el fragment de la derecha no muestra información de ningún lugar concreto.

Ejercicio: Modificar el contenido de  un fragment desde otro.
 

La aplicación creada hasta ahora no funciona como esperamos. En este ejercicio vamos a conseguir que cuando se pulse sobre un elemento de la lista, el fragment de la derecha visualice la información del lugar seleccionado.

1.     Reemplaza en CasosUsoLugare el siguiente método:

public void mostrar(int pos) {
   VistaLugarFragment fragmentVista = obtenerFragmentVista();
   if (fragmentVista != null) {
      fragmentVista.pos = pos;
      fragmentVista._id = lugares.getAdaptador().idPosicion(pos); 
      fragmentVista.actualizaVistas();
   } else {
      Intent intent = new Intent(actividad, VistaLugarActivity.class);
      intent.putExtra("pos", pos);
      actividad.startActivityForResult(intent, 0);
   }
}

public VistaLugarFragment obtenerFragmentVista()  {
   FragmentManager manejador = actividad.getSupportFragmentManager();
   return  (VistaLugarFragment)
           manejador.findFragmentById(R.id.vista_lugar_fragment);
}   
fun mostrar(pos: Int) {
   var fragmentVista  = obtenerFragmentVista()
   if (fragmentVista != null) {
      fragmentVista.pos = pos
      fragmentVista._id = lugares.adaptador.idPosicion(pos)  
      fragmentVista.actualizaVistas()
   } else {
      val i = Intent(actividad, VistaLugarActivity::class.java)
      i.putExtra("pos", pos);
      actividad.startActivity(i);
   }
}

fun obtenerFragmentVista(): VistaLugarFragment? {
   val manejador = actividad.supportFragmentManager
   return manejador.findFragmentById(R.id.vista_lugar_fragment) as
                                               VistaLugarFragment?
}  

Este método ha de visualizar el lugar solicitado. Comenzamos obteniendo una referencia al fragment con id vista_lugar_fragment. Si existe este fragmnet quiere decir que está ahora en pantalla y no es necesario crear una nueva actividad. Simplemente cambiando pos e _id, y llamando al método actualizarVistas() conseguimos que se muestre la información en el fragment ya existente. En caso de que este fragment no exista (esto podrá pasar tras hacer uno de los próximos ejercicios), creamos una nueva actividad para mostrar la información.

2.    En Java para poder acceder a la propiedad pos e id de VistaLugarFragment cambia el modificador private por public.

3.    Observa como aparece un error al tratar de obtener supportFragmentManager. Estamos trabajando con una variable de tipo Activity, pero este método solo está disponible en su descendiente FragmentActivity. Para resolverlo cambia el tipo de la propiedad.

public class CasosUsoLugar {
   private FragmentActivity actividad;
   …
   public CasosUsoLugar(FragmentActivity actividad, LugaresBD lugares … 
class CasosUsoLugar(val actividad: FragmentActivity,
                    val lugares: LugaresBD, …  
Este cambio implica que ya solo podremos usar estos casos de uso desde una actividad de la clase FragmentActivity. Nosotros lo estábamos haciendo desde AppCompactActivity. No vamos a tener problemas al tratarse de un descendiente de FragmentActivity. 
 

4.    Ejecuta la aplicación  y verifica que puedes cambiar el fragment de la derecha.

5.    Si vas a preferencias y cambias el criterio de ordenación y acto seguido modificas la valoración del lugar en pantalla. Es posible que este se duplique en la lista de la izquierda. El problema se debe a que las variables pos y _id de VistaLugarFragment no tenían el valor correcto tras alterar el orden de la lista

6.    Para arreglarlo añade en MainActivity dentro de onActivityResult():
 

if (requestCode == RESULTADO_PREFERENCIAS) {
   adaptador.cursor = lugares.extraeCursor();
   adaptador.notifyDataSetChanged();
   if (usoLugar.obtenerFragmentVista() != null)
      usoLugar.mostrar(0); 
} 

En Kotlin el uso de ; es opcional. Lo que hacemos es averiguar si estamos visualizando dos fragments y en ese caso mostraremos en el fragment de la derecha el primer lugar de la lista. En caso contrario, solo se visualiza la lista y no existe VistaLugarFragment.


  Ejercicio: Adaptar CasosUsoLugar a fragments
 

La clase CasosUsoLugar estaba pensada para ser usada desde una actividad. De hecho, era uno de los parámetros que se pasaban en el constructor. Gracias a este parámetro no solo se extraía el contexto, sino que también se usaba para invocar a startActivityForResult() para arrancar nuevas actividades y que luego se devuelva información a la actividad adecuada.

Pero ahora la situación ha cambiado, estos casos de uso no solo pueden ser utilizados por actividades, sino también por fragments. En este ejercicio vamos a introducir los cambios necesarios para que la respuesta de startActivityForResult() sea recogida por la actividad o fragment que está utilizando la clase.

1.   Añade el siguiente atributo en CasosUsoLugar:

public class CasosUsoLugar {
   protected Fragment fragment;
   …
   public CasosUsoLugar(FragmentActivity actividad, Fragment fragment,
                        LugaresBD lugares, AdaptadorLugaresBD adaptador) {
      this.fragment = fragment;
      …
   } 
open class CasosUsoLugar(
   open val actividad: FragmentActivity,
   open val fragment: Fragment?,
   open val lugares: LugaresBD,
   open val adaptador: AdaptadorLugaresBD) 

La idea es que cuando se use desde una actividad el parámetro fragment se pase como null, y si es desde un fragment se pasarán tanto el parámetro actividad como fragment.

2.   Reemplaza, todas las apariciones de actividad.startActivityForResult por:

if (fragment != null) fragment.startActivityForResult(…);
else                  actividad.startActivityForResult(…); 
fragment?.startActivityForResult(…)
        ?:actividad.startActivityForResult(…) 

Si nos han indicado un fragment llamamos desde este para que nos devuelva el resultado a este. En caso contrario lo hacemos desde la actividad.

3.   En MainActivity, VistaLugarActivity y EdicionLugarActivity añade como nuevo parámetro null:

usoLugar = new CasosUsoLugar(this, null, lugares, adaptador); 
usoLugar = CasosUsoLugar(this, null, lugares, adaptador) 

4.   En VistaLugarFragment y SelectorFragment añade como nuevo parámetro this:

usoLugar = new CasosUsoLugar(getActivity(), this, lugares, adaptador); 
usoLugar = CasosUsoLugar(activity!!, this, lugares, adaptador) 

5.   Para que onActivityResult() se llame en la actividad y en los fragments  has de llamar al super en MainActivity, al principio del método:

super.onActivityResult(requestCode, resultCode, data); 
Ejercicio: Introducir escuchadores manualmente en el fragment.
 

Cuando definimos el layout vista_lugar.xml utilizamos el atributo onClick en varias vistas para asociar métodos que se ejecutan al pulsar sobre la vista. El problema es que estos métodos solo pueden ser definidos en una actividad y no en un fragment. Cuando diseñamos un fragment hemos de conseguir que sea reutilizable, por lo que todo su comportamiento ha definirse en la clase del fragment. Para resolver este problema, vamos a programar los escuchadores manualmente, en lugar de utilizar el atributo onClick.
 

Más información sobre onClick y escuchadores de eventos en[2]
 

1.     Abre el layout vista_lugar.xml y localiza el siguiente fragmento de código. Elimina la línea tachada y asegúrate que coincida el id:

<LinearLayout
     android:id="@+id/barra_url"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:onClick="verPgWeb"
     android:orientation="horizontal" > 

2.     Abre la clase VistaLugarFragment y añade en el método onActivityCreate() el siguiente código. En Java tras obtener v:

v.findViewById(R.id.barra_url).setOnClickListener(new OnClickListener () {
   public void onClick(View view) {  usoLugar.verPgWeb(lugar); } }); 
barra_url.setOnClickListener { usoLugar.verPgWeb(lugar) } 

NOTA: Cuando pulses Alt-Intro  para incluir los imports de las nuevas clases, selecciona el paquete marcado.
 

3.     Ejecuta la aplicación y verifica que al pulsar en la vista de un lugar, sobre la url o su icono se abre la página web correspondiente.

4.     Repite esta operación para todas las vistas del layout donde se haya utilizado el atributo onClick.
 

Ejercicio: Mostrar dos fragments solo con pantallas grandes.
 

Cuando ejecutamos la aplicación en una pantalla pequeña, como la de un teléfono, no tienen ningún sentido mostrar dos fragments simultáneamente. Esto solo nos interesa en una tableta. Para conseguir este doble funcionamiento vamos a trabajar con dos layouts diferentes. Cargaremos uno u otro aprovechando los recursos alternativos de Android.
 

1.    En el explorador del proyecto, pulsa con el botón derecho sobre la carpeta res/layout. y selecciona New > Layout Resource File. En File name: introduce  content_main; en Available qualifiers: selecciona Smallest Screen Width; y el valor 600:
 


 

Se creará la carpeta res/layout-sw600dp. Los recursos de esta carpeta se cargarán cuando la aplicación se ejecute en una pantalla de 7’ o más [3].

2.    Realiza una copia del contenido de  content_main.xml por defecto al nuevo recurso que acabas de crear.

3.    Elimina en el content_main.xml por defecto el segundo de los dos fragments que contiene.

4.    Ejecuta la aplicación en un dispositivo de pantalla pequeña y en uno de más de 7’. Observa cómo se muestra uno o dos fragments según el tamaño de pantalla.
 

Práctica: Simplificación de la actividad VistaLugar.
 

Si comparas el código de la actividad VistaLugarActivity con el de VistaLugarFragment verás que son casi idénticos. Dejar el mismo código en dos clases diferentes es un grave error de programación. Trata de modificar la actividad VistaLugarActivity para que se limite a visualizar el fragment VistaLugarFragment en su interior.
 

Solución:

Crea el Layout activity_vista_lugar.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="horizontal" >
   <fragment
     android:id="@+id/vista_lugar_fragment"
     android:name="com.example.mislugares.presentacion.VistaLugarFragment"
     android:layout_width="0dp"
     android:layout_height="match_parent"
     android:layout_weight="1" 
     android:clickable="true"/>
</LinearLayout> 

En VistaLugarActivity: solo ha de estar el método onCreate() que cargue el layout:

setContentView(R.layout.activity_vista_lugar); 
 

Ejercicio: Ajustando comportamiento al borrar un lugar con fragments.

Dentro de la clase CasosUsoLugar, el código para borrar un lugar termina con:

actividad.finish(); 

Si trabajamos solo en una actividad, el funcionamiento es correcto. Tras borrar el lugar cerramos la actividad dado que no tiene sentido mostrar un lugar que ya no existe. Pero si trabajamos con dos fragments visualizándose juntos, al cerrar la actividad se cerrarán los dos y saldremos de la aplicación. En el presente ejercicio corregiremos este comportamiento no deseado.
 

1.   En el método borrar() reemplaza actividad.finish() por el código siguiente:
 

if (obtenerFragmentSelector() == null) {
   actividad.finish();
} else {
   mostrar(0);
} 
if (obtenerFragmentSelector() == null) {
   actividad.finish()
} else {
   mostrar(0)
}  

Tras borrar el lugar trataremos de averiguar si estamos en una configuración con dos fragments. Esto ocurrirá cuando podemos obtener una referencia de selector_fragment. Si no existe,  realizamos la misma acción de antes. Si existe, hacer que se muestre un lugar con código diferente del borrado, en este caso se muestra el primero de la lista.

2.   Añade la siguiente función:

private SelectorFragment obtenerFragmentSelector() {
   FragmentManager manejador = actividad.getSupportFragmentManager();
   return (SelectorFragment)manejador.findFragmentById(R.id.selector_fragment);
} 
fun obtenerFragmentSelector(): SelectorFragment? {
   val manejador = actividad.supportFragmentManager
   return manejador.findFragmentById(R.id.selector_fragment) as 
                                                         SelectorFragment?
} 
3.    Ejecuta la aplicación y verifica el resultado.

4.   Si borrar todos los lugares de la lista, trabajando con una tableta, verás que se produce un error. Ni siquiera podrás volver a arrancar la aplicación.

5.   Para resolverlo añade en VistaLugarFragment al comienzo de actualizaVistas:

if (lugares.tamaño() == 0) return; 

6.   Ejecuta la aplicación y verifica el resultado.

 

Preguntas de repaso: Fragments
 

[1] http://www.youtube.com/watch?v=NMAJfqDOpBQ

[2] http://youtu.be/OiVePqBmpcQ

[3] http://www.androidcurso.com/images/dcomg/ficheros/recursos_alternativos.pdf

Operaciones con bases de datos en Mis Lugares

En los apartados anteriores hemos aprendido a crear una base de datos y a realizar consultas en una tabla. En este apartado vamos a continuar aprendiendo las operaciones básicas cuando trabajamos con datos. Estas son: altas, bajas y modificaciones y consultas.

Ejercicio: Consulta de un elemento en Mis Lugares

1.     Remplaza en la clase LugaresBD el método elemento() con el siguiente código. Su finalidad es buscar el lugar correspondiente a un id y devolverlo.

@Override public Lugar elemento(int id) {
   Cursor cursor = getReadableDatabase().rawQuery(
                           "SELECT * FROM lugares WHERE _id = "+id, null);
   try {
      if (cursor.moveToNext()) 
         return extraeLugar(cursor);
      else 
         throw new SQLException("Error al acceder al elemento _id = "+id);
   } catch (Exception e) {
      throw e;
   } finally {
      if (cursor!=null) cursor.close();
   }
}  
override fun elemento(id: Int): Lugar {
   val cursor = readableDatabase.rawQuery(
                           "SELECT * FROM lugares WHERE _id = $id", null)
   try {
      if (cursor.moveToNext())
         return extraeLugar(cursor)
      else
         throw SQLException("Error al acceder al elemento _id = $id")
   } catch (e:Exception) {
      throw e
   } finally {
      cursor?.close()
   }
} 

Comenzamos inicializando el valor del lugar a devolver a null, para que corresponda con el valor devuelto en caso de no encontrarse el id. Luego se obtiene el objeto bd llamando a getReadableDatabase(). Este objeto nos permitirá hacer consultas en la base de datos. Por medio del método rawQuery() realiza una consulta en la tabla lugares usando el comando SQL SELECT * FROM lugares WHERE _id =… . Este comando podría interpretarse como, selecciona todos los campos de la tabla lugares, para el registro con el id indicado. El resultado de una consulta es un Cursor con el registro, si es encontrado, o en caso contrario, un Cursor vacío.

En la siguiente línea llamamos cursor.moveToNext() para que el cursor pase a la siguiente fila encontrada. Como es la primera llamada estamos hablando del primer elemento. Devuelve true si lo encuentra y false si no. En caso de encontrarlo llamamos a extraeLugar() para actualizar todos los atributos de lugar con los valores  de la fila apuntada  por el cursor. Si no lo encontramos lanzamos una excepción.

Es importante cerrar lo antes posibles el consumo de memoria. Lo hacemos en la sección finally para asegurarnos que se realiza siempre, haya habido una excepción o no.

Ejercicio: Modificación de un lugar

Si tratas de modificar cualquiera de los lugares observarás que los cambios no tienen efecto. Para que la base de datos sea actualizada, realiza el siguiente ejercicio: 

1.     Añade en la clase LugaresBD en el método actualiza()el siguiente código. Su finalidad es reemplazar el lugar correspondiente al id indicado por un nuevo lugar.

@Override public void actualiza(int id, Lugar lugar) {
   getWritableDatabase().execSQL("UPDATE lugares SET" +
            "   nombre = '" + lugar.getNombre() +
            "', direccion = '" + lugar.getDireccion() +
            "', longitud = " + lugar.getPosicion().getLongitud() +
            " , latitud = " + lugar.getPosicion().getLatitud() +
            " , tipo = " + lugar.getTipo().ordinal() +
            " , foto = '" + lugar.getFoto() +
            "', telefono = " + lugar.getTelefono() +
            " , url = '" + lugar.getUrl() +
            "', comentario = '" + lugar.getComentario() +
            "', fecha = " + lugar.getFecha() +
            " , valoracion = " + lugar.getValoracion() +
            " WHERE _id = " + id);
} 
override fun actualiza(id:Int, lugar:Lugar) = with(lugar) {
   writableDatabase.execSQL("UPDATE lugares SET " +
      "nombre = '$nombre', direccion = '$direccion', " +
      "longitud = ${posicion.longitud}, latitud = ${posicion.latitud}, " +
      "tipo = ${tipoLugar.ordinal}, foto ='$foto', telefono =$telefono, "+
      "url = '$url', comentario = '$comentarios', fecha = $fecha, " +
      "valoracion = $valoracion  WHERE _id = $id")
} 

2.     En  la clase EdicionLugarActivity, en el método onOptionsItemSelected() añade el código subrayado y elimina el tachado:

int _id = lugares.getAdaptador().idPosicion(pos);
usoLugar.guardar(pos _id, lugar); 
val _id = lugares.adaptador.idPosicion(pos)
usoLugar.guardar(pos _id, nuevoLugar) 

La variable pos corresponde a un indicador de posición dentro de la lista. Para utilizar correctamente el método actualiza() de LugaresBD, hemos de obtener el _id correspondiente a la primera columna de la tabla. Este cambio lo realiza el método idPosicion() de adaptador.

3.    Ejecuta la aplicación y trata de modificar algún lugar. Observa que, cuando realizas un cambio en un lugar, estos no parecen en VistaLugarActivity ni en MainActivity. Realmente sí que se han almacenado, el problema está en que el adaptador no se ha actualizado. Para verificar que los cambios sí que se han almacenado puedes cambiar el criterio de ordenación, para que se actualice el adaptador.

4.   Para resolver el refresco de MainActivity has de sobrescribir el método actuliza() en LugaresDBAdapter.

@Override public void actualiza(int id, Lugar lugar) {
   super.actualiza(id,lugar);
   adaptador.setCursor(extraeCursor());
   adaptador.notifyDataSetChanged();
}
override fun actualiza(id:Int, lugar:Lugar) {
   super.actualiza(id,lugar)
   adaptador.setCursor(extraeCursor())
   adaptador.notifyDataSetChanged()
} 

Empezamos llamando al super para que el lugar se actualice en la base de datos según se indica en LugaresBD. Luego creamos un nuevo cursor, dado que al cambiar el lugar es posible que cambie el orden de los elementos seleccionados. Finalmente notificamos al adaptador que los datos han cambiado que vuelva a pintar las vistas correspondientes del RecyclerView.

5.    Ejecuta de nuevo la aplicación. Tras editar un lugar, los cambios se reflejan en <MainActivity pero no en VistaLugarActivity.

6.    Para resolver este problema has de añadir las siguientes líneas en VistaLugarActivity

private int _id = -1;

@Override public void onCreate(…) {
   …
   pos = extras.getInt("pos", 0);
   _id = lugares.getAdaptador().idPosicion(pos);
   …
@Override public void onActivityResult(…) {
   if (requestCode == RESULTADO_EDITAR) {
      lugar = lugares.elemento(_id);
      pos = lugares.getAdaptador().posicionId(_id);
      actualizaVistas();
   } 
private var _id: Int = -1

override fun onCreate(…) {
   …
   pos = intent.extras?.getInt("pos", 0) ?: 0
   _id = lugares.adaptador.idPosicion(pos)
   …
override fun onActivityResult(…) {
   if (requestCode == RESULTADO_EDITAR) {
      lugar = lugares.elemento(_id)
      pos = lugares.adaptador.posicionId(_id)
      actualizaVistas()
   }… 

Necesitamos actualizar la variable lugar dado que esta acaba de ser modificada. Extraerla según su posición en el listado es potencialmente peligroso, dado que esta posición puede cambiar dinámicamente. Por ejemplo, si ordenamos los lugares por orden alfabético y modificamos su inicial, posiblemente cambie su posición. Por el contrario, el _id de un lugar nunca puede cambiar. Hemos obtenido el _id al crear la actividad. Tras la edición del lugar, con este _id, obtenemos los nuevos valores para lugar y buscamos la nueva posición a partir de _id.

7.    Para hacer la última acción añade en AdaptadorLugaresBD la siguiente función:

public int posicionId(int id) {
    int pos = 0;
    while (pos<getItemCount() && idPosicion(pos)!=id) pos++;
    if (pos >= getItemCount()) return -1;
    else                       return pos;
} 
fun posicionId(id: Int): Int {
   var pos = 0
   while (pos < itemCount && idPosicion(pos) != id) pos++
   return if (pos >= itemCount) -1
          else                   pos
}  

Como ves se recorren todos los elementos del adaptador hasta encontrar uno con el mismo id. Si no es encontrado devolvemos -1.

8.    Ejecuta la aplicación y verifica el nuevo funcionamiento.


Ejercicio: Modificación valoración y fotografía de un lugar

1.    Algunos de los campos de un lugar no se modifica en la actividad EdicionLugarActivity, si no que se hacen directamente en VistaLugarActivity. En concreto la valoración, la fotografía y más adelante añadiremos fecha y hora. Cuando se modifiquen estos campos, también habrá que almacenarlos de forma permanente en la base de datos. Empezaremos por la valoración. Añade en el método actualizaVistas() de VistaLugarActivity el código subrayado.

valoracion.setOnRatingBarChangeListener(null);
valoracion.setRating(lugar.getValoracion());
valoracion.setOnRatingBarChangeListener(
        new RatingBar.OnRatingBarChangeListener() {
           @Override
           public void onRatingChanged(RatingBar ratingBar,
                                       float valor, boolean fromUser) {
              lugar.setValoracion(valor);
              pos = lugares.actualizaPosLugar(pos, lugar);
           }
        }); 
valoracion.setOnRatingBarChangeListener { _, _, _ -> }
valoracion.setRating(lugar.valoracion)
valoracion.setOnRatingBarChangeListener { _, valor, _ ->
   lugar.valoracion = valor
   pos = lugares.actualizaPosLugar(pos, lugar)
} 

Cuando el usuario cambie la valoración de un lugar se llamará a onRatingChanged()donde actualizamos la valoración y llamamos a actualizaLugares(). Esta función llamará a actualizaVistas(), donde cambiamos raingBar, lo que provocará una llamada al escuchador, y así sucesivamente entrando en bucle. Para evitarlo antes de cambiar el valor desactivamos el escuchador. Si tenemos seleccionada la ordenación por valoración, al cambiarla puede cambiar la posición del lugar en la lista. Por si ha cambiado, volvemos a obtener la variable pos.

2.     Añade la siguiente función a LugaresBDAdapter:

public int actualizaPosLugar(int pos, Lugar lugar) {
   int id = adaptador.idPosicion(pos);
   actualiza(id, lugar);
   return adaptador.posicionId(id); //devolvemos la nueva posición
} 
fun actualizaPosLugar(pos: Int, lugar: Lugar): Int {
   val id = adaptador.idPosicion(pos)
   actualiza(id, lugar)
   return adaptador.posicionId(id)  //devolvemos la nueva posición
}

Primero obtenemos en la variable id el identificador del lugar. Para ello, vamos a usar la posición que el lugar ocupa en el listado. Con el id, ya podemos actualizar la base de datos. La función devuelve la nueva posición que el lugar tiene en el adaptador.

3.     Para que los cambios en las fotografías se actualicen también has obtener el lugar de forma adecuada y llamar a actualizaPosLugar():

public void ponerFoto(int pos, String uri, ImageView imageView) {
   Lugar lugar = lugares.elemento adaptador.lugarPosicion(pos);
   lugar.setFoto(uri);
   visualizarFoto(lugar, imageView);
   lugares.actualizaPosLugar(pos, lugar);
} 
fun ponerFoto(pos: Int, uri: String?, imageView: ImageView) {
   val lugar = lugares.elemento adaptador.lugarPosicion(pos)
   lugar.foto = uri ?: ""
   visualizarFoto(lugar, imageView)
   lugares.actualizaPosLugar(pos, lugar)
} 

4.     Verifica que tanto los cambios de valoración como de fotografía se almacenan correctamente.


Ejercicio: Alta de un lugar

En este ejercicio aprenderemos a añadir nuevos registros a la base de datos. 

1.     Reemplaza en la clase LugaresBD el método nuevo() por el siguiente. Su finalidad es crear un nuevo lugar en blanco y devolver el id del nuevo lugar.

   @Override public int nuevo() {
   int _id = -1;
   Lugar lugar = new Lugar();
   getWritableDatabase().execSQL("INSERT INTO lugares (nombre, " +
       "direccion, longitud, latitud, tipo, foto, telefono, url, " +
       "comentario, fecha, valoracion) VALUES ('', '',  " +
       lugar.getPosicion().getLongitud() + ","+
       lugar.getPosicion().getLatitud() + ", "+ lugar.getTipo().ordinal()+
       ", '', 0, '', '', " + lugar.getFecha() + ", 0)");
    Cursor c = getReadableDatabase().rawQuery(
       "SELECT _id FROM lugares WHERE fecha = " + lugar.getFecha(), null);
    if (c.moveToNext()) _id = c.getInt(0);
    c.close();
    return _id;
}
   override fun nuevo():Int {
   var _id = -1
   val lugar = Lugar(nombre = "")
   writableDatabase.execSQL("INSERT INTO lugares (nombre, direccion, " +
       "longitud, latitud, tipo, foto, telefono, url, comentario, " +
       "fecha, valoracion) VALUES ('', '', ${lugar.posicion.longitud}, " + 
       "${lugar.posicion.latitud}, ${lugar.tipoLugar.ordinal}, '', 0, " +
       "'', '', ${lugar.fecha},0 )")
   val c = readableDatabase.rawQuery((
       "SELECT _id FROM lugares WHERE fecha = " + lugar.fecha), null)
   if (c.moveToNext()) _id = c.getInt(0)
   c.close()
   return _id
}  

Comenzamos inicializando el valor del _id a devolver a -1. De esta manera, si hay algún problema este será el valor devuelto. Luego se crea un nuevo objeto Lugar. Si consultas el constructor de la clase, observarás que solo se inicializan posicion, tipo y fecha. El resto de los valores serán una cadena vacía para String y 0 para valores numéricos. Acto seguido, se crea una nueva fila con esta información. Los valores de texto y numéricos tampoco se indican, al inicializarse de la misma manera.
El método ha de devolver el _id del elemento añadido. Para conseguirlo se realiza una consulta buscando una fila con la misma fecha que acabamos de introducir.

2.    Para la acción de añadir vamos a utilizar el botón flotante que tenemos desde la primera versión de la aplicación. Abre el fichero res/layout/activity_main.xml y reemplaza el icono aplicado a este botón:


<android.support.design.widget.FloatingActionButton
    … 
    android:src="@android:drawable/ic_input_add"
    … /> 

3.   Abre la clase MainActivity y dentro de onCreate() comenta el código tachado y añade el subrayado, para que se ejecute al pulsar el botón flotante:

fab.setOnClickListener(new View.OnClickListener() {
   @Override public void onClick(View view) {
      Snackbar.make(view,"Replace with your own action",Snackbar.LENGTH_LONG)
              .setAction("Action", null).show();
      usoLugar.nuevo();
   }
});
fab.setOnClickListener { view ->
   Snackbar.make(view, "Replace with your own action",Snackbar.LENGTH_LONG)
      .setAction("Action", null).show()
   usoLugar.nuevo()
} 

4.  Añade el siguiente caso de uso en CasoUsoLugar:

   public void nuevo() {
   int id = lugares.nuevo();
   GeoPunto posicion = ((Aplicacion) actividad.getApplication())
           .posicionActual;
   if (!posicion.equals(GeoPunto.SIN_POSICION)) {
      Lugar lugar = lugares.elemento(id);
      lugar.setPosicion(posicion);
      lugares.actualiza(id, lugar);
   }
   Intent i = new Intent(actividad, EdicionLugarActivity.class);
   i.putExtra("_id", id);
   actividad.startActivity(i);
}
fun nuevo() {
   val _id = lugares.nuevo()
   val posicion = (actividad.application as Aplicacion).posicionActual
   if (posicion != GeoPunto.SIN_POSICION) {
      val lugar = lugares.elemento(_id)
      lugar.posicion = posicion
      lugares.actualiza(_id, lugar)
   }
   val i = Intent(actividad, EdicionLugarActivity::class.java)
   i.putExtra("_id", _id)
   actividad.startActivity(i)
}  

Comenzamos creando un nuevo lugar en la base e datos cuyo identificaor va a ser _id. La siguiente línea obtiene la posición actual. Si el dispositivo está localizado, obtenemos el lugar recién creado, cambiamos su posición y lo volvemos a guardar. A continuación vamos a lanzar la actividad EdicionLugarActivity para que el usuario rellene los datos del lugar. Hasta ahora hemos utilizado el extra "pos"“id” para indicar la posición en la lista del objeto a editar. Pero ahora esto no es posible, dado que este nuevo lugar no ha sido añadido a la lista. Para resolver el problema vamos a crear un nuevo extra, “_id”, que usaremos para identificar el lugar a editar por medio de su campo _id

5.     En la clase EdicionLugarActivity añade el código subrayado:

private int _id;

@Override protected void onCreate(Bundle savedInstanceState) {
   …
   Bundle extras = getIntent().getExtras();
   pos = extras.getInt("pos", -1) ;
   _id = extras.getInt("_id", -1);
   if (_id!=-1) lugar = lugares.elemento(_id);
   else         lugar = lugares.elementoPos(pos);
   actualizaVistas();
} 
var _id = -1

override fun onCreate(savedInstanceState: Bundle?) {
   …
   pos = intent.extras?.getInt("pos", -1) ?: -1
   _id = intent.extras?.getInt("_id", -1) ?: -1
   lugar = if (_id !== -1) lugares.elemento(_id)
           else            lugares.elementoPos(pos)
   actualizaVistas()
} 

Esta actividad va a poder ser llamada de dos formas alternativas: usando el extra “pos”“id” para indicar que el lugar a modificar ha de extraerse de una posición del adaptador; o usando “_id” en este caso el lugar será extraido de la base de datos usando su identificador. Observa como se han definido dos variables globales, id e “pos” e _id. Aunque solo una se va a inicializar y la otra valdrá -1.  

6.     Cuando el usuario pulse la opción guardar se llamará al método onOptionsItemSelected(). Para almacenar la información tendremos que verificar cual de las dos variables ha sido inicializada. Añade el código subrayado en este método:

case R.id.accion_guardar:	
   …
   if (_id==-1) int _id = lugares.getAdaptador().idPosicion(pos);
   usoLugar.guardar(_id, lugar);
   finish();
   return true; 
R.id.accion_guardar -> {
   …
   if (_id==-1) val _id = lugares.adaptador.idPosicion(pos)
   usoLugar.guardar(_id, nuevoLugar)
   finish()
   return true
} 

El if es añadido dado que si nos han pasado el identificador _id ya no tiene sentido obtenerlo a partir de la posición.

7.     Verifica que los cambios introducidos funcionan correctamente.


Ejercicio: Baja de un lugar

En este ejercicio aprenderemos a eliminar filas de la base de datos.

1.     Reemplaza en la clase LugaresBD el método borrar() por el siguiente. Su finalidad es eliminar el lugar correspondiente al id indicado.

   public void borrar(int id) {
   getWritableDatabase().execSQL("DELETE FROM lugares WHERE _id = " + id);
} 
override fun borrar(id: Int) {
   writableDatabase.execSQL("DELETE FROM lugares WHERE _id = $id")
} 

2.     Añade en la clase VistaLugarActivity, dentro del método onOptionsItemSelected(), el código subrayado:

case R.id.accion_borrar:
    usoLugar.borrarPos(pos);
    return true;
R.id.accion_borrar -> {
   usoLugar.borrarPos(pos)
   return true
} 

3.     En CasosUsoLugar,añade la función:

public void borrarPos(int pos) {
   int id = lugares.getAdaptador().idPosicion(pos);
   borrar(id);
}
fun borrarPos(pos: Int) {
   val id = lugares.adaptador.idPosicion(pos)
   borrar(id)
} 

Tiene la misma finalidad que la función borrar(), pero indicando la posición.

4.     Dentro de borrar(), añade las dos líneas subrayadas para actualizar el cursor y notificar al adaptador que los datos han cambiado:

lugares.borrar(id);
lugares.getAdaptador().setCursor(lugares.extraeCursor());
lugares.getAdaptador().notifyDataSetChanged();
actividad.finish();
lugares.borrar(id)
lugares.adaptador.cursor = lugares.extraeCursor()
lugares.adaptador.notifyDataSetChanged()
actividad.finish() 

5.     Ejecuta la aplicación y trata de dar de baja algún lugar.


  Ejercicio: Opción CANCELAR en el alta de un lugar

Si seleccionas la opción nuevo y en la actividad EdicionLugarActivity seleccionas la opción CANCELAR, puedes verificar que esta opción funciona mal. Los datos introducidos no se guardarán; sin embargo, se creará un nuevo lugar con todos sus datos en blanco. Para verificarlo has de salir de la aplicación para que se recargue el adaptador.

Para evitar este comportamiento, borra el elemento nuevo cuando se seleccione la opción CANCELAR. Pero este comportamiento ha de ser diferente cuando el usuario entró en la actividad EdicionLugarActivity para editar un lugar ya existente. Para diferenciar estas dos situaciones puedes utilizar los extras _id y pos.

Solución:

Añade en el método onOptionsItemSelected() en la opción accion_cancelar:

if (_id!=-1) usoLugares.borrar(_id) 
Preguntas de repaso:  SQLite II

Adaptadores para bases de datos

Un adaptador (Adapter) es un mecanismo de Android que hace de puente entre nuestros datos y las vistas contenidas en un ListView (o un GridView o Spinner). Dado que es muy frecuente usar bases de datos e nuestras aplicaciones, existen adaptadores específicos para este caso. La forma de trabajar habitual es hacer una consulta en una base de datos (comando SQL SELECT) y obtener un objeto Cursor con el resultado. Este objeto será pasado al adaptador para que lo recorra y cree tantas vistas como registros se hayan encontrado.

La forma más sencilla de pasar los datos del Cursor obtenido en una consulta a un ListView es utilizar la clase SimpleCursorAdapter. En una lección acterior vimos un ejemplo que usaba el siguiente código:

adaptador = new SimpleCursorAdapter(this,
      R.layout.elemento_lista,
      Lugares.listado(),
      new String[] { "nombre", "direccion"},
      new int[] { R.id.nombre, R.id.direccion},
   0);

Los parámetros necesarios son: el contexto de nuestra aplicación, un layout con el diseño básico que queremos repetir, un objeto Cursor con una consulta a nuestra base de datos, una lista con los nombres de los registros que queremos visualizar en cada layout, una lista con los id de recurso de los elementos del layout donde queremos visualizar estos datos y finalmente un campo de opciones.

Este tipo de adaptador tiene una restricción, los id de los elementos a reemplazar solo pueden ser del tipo TextView o ImageView. Por lo que cosas más complejas como modificar el valor de un RatingBar no son posibles.

Cuando queramos definir un Adaptador sin ningún tipo de restricciones aprendimos a extender la clase BaseAdapter. A continuación aprenderemos a usar otra clase sin restricciones, pero que nos facilita mucho del trabajo a realizar cuando los datos a mostrar vienen de un objeto Cursor. Se trata de la clase CursorAdapter.

Ejercicio: Utilizando un CursorAdapter en Mis Lugares

1.     Crea la clase AdaptadorCursorLugares en MisLugares con el siguiente código:

public class AdaptadorCursorLugares extends CursorAdapter {
   private LayoutInflater inflador; // Crea Layouts a partir del XML
   TextView nombre, direccion, distancia;
   ImageView foto;
   RatingBar valoracion;

   public AdaptadorCursorLugares(Context contexto, Cursor c) {
      super(contexto, c, false);
   }

   @Override
   public View newView(Context contexto, Cursor c, ViewGroup padre) {
      inflador = (LayoutInflater) contexto

            .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
      View vista = inflador.inflate(R.layout.elemento_lista, padre, false);
      return vista;
   }

   @Override
    public void bindView(View vista, Context contexto, Cursor c) {
      nombre = (TextView) vista.findViewById(R.id.nombre);
      direccion = (TextView) vista.findViewById(R.id.direccion);
      foto = (ImageView) vista.findViewById(R.id.foto);
      valoracion = (RatingBar) vista.findViewById(R.id.valoracion);
      nombre.setText(c.getString(c.getColumnIndex("nombre")));
      direccion.setText(c.getString(c.getColumnIndex("direccion")));
      int tipo = c.getInt(c.getColumnIndex("tipo"));
      foto.setImageResource(TipoLugar.values()[tipo].getRecurso());
      foto.setScaleType(ImageView.ScaleType.FIT_END);
      valoracion.setRating(c.getFloat(c.getColumnIndex("valoracion")));
      distancia = (TextView) vista.findViewById(R.id.distancia);
    GeoPunto posicion = new GeoPunto(
        c.getDouble(c.getColumnIndex("longitud")),
        c.getDouble(c.getColumnIndex("latitud")));
      if (Lugares.posicionActual != null && posicion != null
            && posicion.getLatitud() != 0) {
         int d = (int) Lugares.posicionActual.distancia(posicion);
         if(d < 2000) {
            distancia.setText(d + " m");
         } else {
            distancia.setText(d / 1000 + "Km");
         }
      }
   }

}

NOTA: Para añadir los import adecuados pulsa Alt-Intro en Android Studio o Ctrl-Shift-O en Eclipse.  Cuando te pregunte sobre el paquete de CursorAdapter selecciona: android.support.v4.widget.CursorAdapter

Para extender un CursorAdapter hemos de seguir los siguientespasos:

Primero, llamamos al constructor indicando el contexto, el cursor con la consulta y un booleano indicando si la consulta ha de ser regenerada (se recomienda el valor false).

Luego, sobreescribimos el método newView() para crear cada vista de la consulta. En este método no hay que rellenar los datos correspondientes solo crear las vistas.

Finalmente, sobreescribimos el método bindView() para rellenar los datos de cada vista. Se nos pasa la vista creada de la llamada anterior y un cursor situado en el elemento que nos toca rellenar en este momento. Con esta información resulta muy fácil rellenar los diferentes elementos. Por ejemplo, el siguiente código pone el texto en la vista nombre utilizando el String obtenido en la posición actual del cursor, tomando el valor del segundo campo o columna. Si vas a la definición de la tabla lugares en primer lugar está el campo _id, en segundo lugar nombre, en tercero dirección, Además ten en cuenta que se empieza a numerar desde el cero.

nombre.setText(c.getString(1));

Esta forma de trabajar resulta poco legible, además el orden de las columnas puede cambiar en un SELECT donde no se seleccionen todas. En el código hemos preferido usar la siguiente construcción:

nombre.setText(c.getString(c.getColumnIndex("nombre")));

El método getColumnIndex() buscará la columna por su nombre devolviendo su índice.

2.     Abre la clase MainActivity y en el método onCreate() comenta las líneas que inicilizan adaptador y reemplazalas por la siguiente:

adaptador = new AdaptadorCursorLugares(this, Lugares.listado());

3.     Ejecuta la aplicación y verifica que las vistas se muestra correctamente.

Uso de base de datos en Android

Las bases de datos son una herramienta de gran potencia en la creación de aplicaciones informáticas. Hasta hace muy poco resultaba costoso y complejo utilizar bases de datos en nuestras aplicaciones. No obstante, Android incorpora la librería SQLite, que nos permitirá utilizar bases de datos mediante el lenguaje SQL, de una forma sencilla y utilizando muy pocos recursos del sistema. Como verás en este apartado, almacenar tu información en una base de datos no es mucho más complejo que almacenarla en un fichero, y además resulta mucho más potente.

SQL es el lenguaje de programación más utilizado para bases de datos. No resulta complejo entender los ejemplos que se mostrarán en este libro. No obstante, si deseas hacer cosas más complicadas te recomiendo que consultes alguno de los muchos manuales que se han escrito sobre el tema.

Para manipular una base de datos en Android usaremos la clase abstracta SQLiteOpenHelper, que nos facilita tanto la creación automática de la base de datos como el trabajar con futuras versiones de esta base de datos. Para crear un descendiente de esta clase hay que implementar los métodos onCreate() y onUpgrade(), y opcionalmente, onOpen(). La gran ventaja de utilizar esta clase es que ella se preocupará de abrir la base de datos si existe, o de crearla si no existe. Incluso de actualizar la versión si decidimos crear una nueva estructura de la base de datos. Además, esta clase tiene dos métodos: getReadableDatabase() y getWritableDatabase(), que abren la base de datos en modo solo lectura o lectura y escritura. En caso de que todavía no exista la base de datos, estos métodos se encargarán de crearla.

En los próximos ejercicios pasamos a demostrar cómo guardar los datos de la aplicación Mis Lugares en una base de datos. Esta estará formada por una única tabla (lugares). A continuación, se muestran las columnas que contendrán y las filas que se introducirán como ejemplo. Los valores que aparecen en las columnas _id y fecha no coincidirán con los valores reales:

>video[Tutorial] Uso de bases de datos en Android

_id

nombre

direccion

longitud

latitud

tipo

foto

telefono

url

Comentario

fecha

valoracion

1

Escuela

C/ Paran

-0.166

38.99

7

962849

ht

Uno de lo

2345

3.0

2

Al de

P. Industr

-0.190

38.92

2

636472

ht

No te pier

2345

3.0

4

android

ciberesp

0.0

0.0

7

ht

Amplia tu

2345

5.0

7

Barranc

Vía Verd

-0.295

38.86

9

ht

Espectacu

2345

4.0

5

La Vital

Avda. de

-0.172

38.97

6

962881

ht

El típico c

2345

2.0

Estructura de la tabla lugares de la base de datos lugares.

Ejercicio: Utilizando una base de datos en Mis Lugares.

1.     Comenzamos haciendo una copia del proyecto, dado que en la nueva versión se eliminará parte del código desarrollado y es posible que queramos consultarlo en un futuro. Abre en el explorador de ficheros  la carpeta que contiene el proyecto.  Para hacer esto puedes pulsar con el botón derecho sobre app en el explorador del proyecto y seleccionar Show in Explorer. Haz una copia de esta carpeta con un nuevo nombre.

2.     Crea la clase LugaresBD, en el paquete datos, con el siguiente código:

public class LugaresBD extends SQLiteOpenHelper {

   Context contexto;

   public LugaresBD(Context contexto) {
      super(contexto, "lugares", null, 1);
      this.contexto = contexto;
   }

   @Override public void onCreate(SQLiteDatabase bd) {
      bd.execSQL("CREATE TABLE lugares ("+
             "_id INTEGER PRIMARY KEY AUTOINCREMENT, "+
             "nombre TEXT, " +
             "direccion TEXT, " +
             "longitud REAL, " +  
             "latitud REAL, " +   
             "tipo INTEGER, " +
             "foto TEXT, " +
             "telefono INTEGER, " +
             "url TEXT, " +
             "comentario TEXT, " +
             "fecha BIGINT, " +
             "valoracion REAL)");  
     bd.execSQL("INSERT INTO lugares VALUES (null, "+
       "'Escuela Politécnica Superior de Gandía', "+
       "'C/ Paranimf, 1 46730 Gandia (SPAIN)', -0.166093, 38.995656, "+
       TipoLugar.EDUCACION.ordinal() + ", '', 962849300, "+ 
       "'http://www.epsg.upv.es', "+
       "'Uno de los mejores lugares para formarse.', "+ 
       System.currentTimeMillis() +", 3.0)");
     bd.execSQL("INSERT INTO lugares VALUES (null, 'Al de siempre', "+
       "'P.Industrial Junto Molí Nou - 46722, Benifla (Valencia)', "+
       " -0.190642, 38.925857, " +  TipoLugar.BAR.ordinal() + ", '', "+ 
       "636472405, '', "+"'No te pierdas el arroz en calabaza.', " + 
       System.currentTimeMillis() +", 3.0)");
     bd.execSQL("INSERT INTO lugares VALUES (null, 'androidcurso.com', "+
       "'ciberespacio', 0.0, 0.0,"+TipoLugar.EDUCACION.ordinal()+", '', "+ 
       "962849300, 'http://androidcurso.com', "+
       "'Amplia tus conocimientos sobre Android.', "+ 
       System.currentTimeMillis() +", 5.0)");
   bd.execSQL("INSERT INTO lugares VALUES (null,'Barranco del Infierno',"+
       "'Vía Verde del río Serpis. Villalonga (Valencia)', -0.295058, "+
       "38.867180, "+TipoLugar.NATURALEZA.ordinal() + ", '', 0, "+ 
      "'http://sosegaos.blogspot.com.es/2009/02/lorcha-villalonga-via-verde-del-"+
       "rio.html', 'Espectacular ruta para bici o andar', "+ 
       System.currentTimeMillis() +", 4.0)");
     bd.execSQL("INSERT INTO lugares VALUES (null, 'La Vital', "+
      "'Avda. La Vital,0 46701 Gandia (Valencia)',-0.1720092,38.9705949,"+
       TipoLugar.COMPRAS.ordinal() + ", '', 962881070, "+
       "'http://www.lavital.es', 'El típico centro comercial', "+ 
       System.currentTimeMillis() +", 2.0)");
   }

   @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, 
                                                      int newVersion) {
   }
} 
class LugaresBD(val contexto: Context) :
                          SQLiteOpenHelper(contexto, "lugares", null, 1) {
   override fun onCreate(bd: SQLiteDatabase) {
     bd.execSQL("CREATE TABLE lugares (" +
          "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
          "nombre TEXT, " +
          "direccion TEXT, " +
          "longitud REAL, " +
          "latitud REAL, " +
          "tipo INTEGER, " +
          "foto TEXT, " +
          "telefono INTEGER, " +
          "url TEXT, " +
          "comentario TEXT, " +
          "fecha BIGINT, " +
          "valoracion REAL)")
    bd.execSQL(("INSERT INTO lugares VALUES (null, " +
       "'Escuela Politécnica Superior de Gandía', " +
       "'C/ Paranimf, 1 46730 Gandia (SPAIN)', -0.166093, 38.995656, "+
       TipoLugar.EDUCACION.ordinal + ", '', 962849300, " +
       "'http://www.epsg.upv.es', " +
       "'Uno de los mejores lugares para formarse.', " +
       System.currentTimeMillis() + ", 3.0)"))
    bd.execSQL(("INSERT INTO lugares VALUES (null, 'Al de siempre', " +
       "'P.Industrial Junto Molí Nou - 46722, Benifla (Valencia)', " +
       " -0.190642, 38.925857, " + TipoLugar.BAR.ordinal + ", '', " +
       "636472405, '', " + "'No te pierdas el arroz en calabaza.', " +
       System.currentTimeMillis() + ", 3.0)"))
    bd.execSQL(("INSERT INTO lugares VALUES (null, 'androidcurso.com', "+
      "'ciberespacio', 0.0, 0.0,"+TipoLugar.EDUCACION.ordinal+", '', "+
      "962849300, 'http://androidcurso.com', " +
      "'Amplia tus conocimientos sobre Android.', " +
      System.currentTimeMillis() + ", 5.0)"))
    bd.execSQL(("INSERT INTO lugares VALUES (null,'Barranco del Infierno',"+
      "'Vía Verde del río Serpis. Villalonga (Valencia)', -0.295058, "+
      "38.867180, " + TipoLugar.NATURALEZA.ordinal + ", '', 0, " +
      "'http://sosegaos.blogspot.com.es/2009/02/lorcha-villalonga-via-verde-del-"+
      "rio.html', 'Espectacular ruta para bici o andar', " +
      System.currentTimeMillis() + ", 4.0)"))
    bd.execSQL(("INSERT INTO lugares VALUES (null, 'La Vital', " +
      "'Avda. La Vital,0 46701 Gandia (Valencia)',-0.1720092,38.9705949,"+
      TipoLugar.COMPRAS.ordinal + ", '', 962881070, " +
      "'http://www.lavital.es', 'El típico centro comercial', " +
      System.currentTimeMillis() + ", 2.0)"))
   }

   override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, 
                                              newVersion: Int) {}
} 

El constructor de la clase se limita a llamar al constructor heredado. Los parámetros se describen a continuación:
 

contexto: Contexto usado para abrir o crear la base de datos.

nombre: Nombre de la base de datos que se creará. En nuestro caso, “puntuaciones”.

cursor: Se utiliza para crear un objeto de tipo cursor. En nuestro caso no lo necesitamos.

version: Número de versión de la base de datos empezando desde 1. En el caso de que la base de datos actual tenga una versión más antigua se llamará a onUpgrade() para que actualice la base de datos.

El método onCreate() se invocará cuando sea necesario crear la base de datos. Como parámetro se nos pasa una instancia de la base de datos que se acaba de crear. Este es el momento de crear las tablas que contendrán información. El primer campo tiene por nombre _id y será un entero usado como clave principal. Su valor será introducido de forma automática por el sistema, de forma que dos registros no tengan nunca el mismo valor.

En nuestra aplicación necesitamos solo la tabla lugares, que es creada por medio del comando SQL CREATE TABLE lugares… La primera columna tiene por nombre _id y será un entero usado como clave principal. Su valor será introducido automáticamente por el sistema, de forma que dos filas no tengan nunca el mismo valor de _id.

Las siguientes líneas introducen nuevas filas en la tabla utilizando el comando SQL INSERT INTO lugares VALUES ( , , … ). Los valores deben introducirse en el mismo orden que las columnas. La primera columna se deja como null dado que corresponde al _id y es el sistema quien ha de averiguar el valor correspondiente. Los valores de tipo TEXT deben introducirse entre comillas, pudiendo utilizar comillas dobles o simples. Como en Java se utilizan comillas dobles, en SQL utilizaremos comillas sencillas. El valor TipoLugar.EDUCACION.ordinal() corresponde a un entero según el orden en la definición de este enumerado y System.currentTimeMillis() corresponde a la fecha actual representada como número de milisegundos transcurridos desde 1970. El resto de los valores son sencillos de interpretar.
Ha de quedar claro que este constructor solo creará una base de datos (llamando a onCreate()) siestá todavía no existe. Si ya fue creada en una ejecución anterior, nos devolverá la base de datos existente.

El método onUpgrade() está vacío. Si más adelante, en una segunda versión de Mis Lugares, decidiéramos crear una nueva estructura para la base de datos, tendríamos que indicar un número de versión superior, por ejemplo la 2. Cuando se ejecute el código sobre un sistema que disponga de una base de datos con la versión 1, se invocará el método onUpgrade(). En él tendremos que escribir los comandos necesarios para transformar la antigua base de datos en la nueva, tratando de conservar la información de la versión anterior.

3.     Para acceder a los datos de la aplicación se definió la interfaz RepositorioLugares. Vamos a implementar esta interfaz para que los cambios sean los mínimos posibles. Añade el texto subrayado a la clase:

public class LugaresBD extends SQLiteOpenHelper
                       implements RepositirioLugares { 
class LugaresBD(val contexto: Context) :
      SQLiteOpenHelper(contexto, "lugares", null, 1), RepositorioLugares { 

Aparecerá un error justo en la línea que acabas de introducir. Si sitúas el cursor de texto sobre el error, aparecerá una bombilla roja con opciones para resolver el error. Pulsa en “Implement methods”, selecciona todos los métodos y pulsa OK. Observa como en la clase se añaden todos los métodos de esta interfaz. De momento vamos a dejar estos métodos sin implementar. En la sección  “Operaciones con bases de datos en Mis Lugares” aprenderemos a realizar las operaciones básicas cuando trabajamos con datos: altas, bajas, modificaciones y consultas.

4.     No ejecutes todavía la aplicación. Hasta que no hagamos el siguiente ejercicio no funcionará correctamente.

Adaptadores para bases de datos

Un adaptador (Adapter) es un mecanismo de Android que hace de puente entre nuestros datos y las vistas contenidas en un RecyclerView,ListView, GridView o Spinner.

En el siguiente ejercicio vamos a crear un adaptador que toma la información de la base de datos que acabamos de crear y se la muestra a un RecyclerView. Realmente podríamos usar el adaptador AdaptadorLugares que ya tenemos creado. Este adaptador toma la información de un objeto que sigue la interfaz RepositorioLugares, restricción que cumple la clase LugaresBD. No obstante, vamos a realizar una implementación alternativa. La razón es que la implementación actual de AdaptadorLugares necesitaría una consultas a la base de datos cada vez que requiera una información de Lugares. (Veremos más adelante que cada llamada a elemento(), añade(), nuevo(), … va a suponer un accedo a la base de datos).

El nuevo adaptador, AdaptadorLugaresBD, va a trabajar de una forma más eficiente. Vamos a realizar una consulta de los elementos a listar y los va a guardar en un objeto de la clase Cursor. Mantendrá esta información mientras no cambie la información a listar, por lo que solo va a necesitar una consulta a la base de datos. En la aplicación el Cursor será cargado al mostrar el listado inicial en MainActivity. Cuando el usuario quiera mostrar el detalle de algún lugar, no será necesario hacer una nueva consulta a la base de datos, dado que la información ya está en el Cursor.

Ejercicio: Un Adaptador para base de datos en Mis Lugares

1.     Crea la clase AdaptadorLugaresBD, en el paquete presentacion, con el siguiente código:

public class AdaptadorLugaresBD extends AdaptadorLugares {

    protected Cursor cursor;

    public AdaptadorLugaresBD(RepositorioLugares 
                                                lugares, Cursor cursor) {
        super(lugares);
        this.cursor = cursor;
    }

    public Cursor getCursor() {
        return cursor;
    }

    public void setCursor(Cursor cursor) {
        this.cursor = cursor;
    }

    public Lugar lugarPosicion(int posicion) {
        cursor.moveToPosition(posicion);
        return LugaresBD.extraeLugar(cursor);
    }

    public int idPosicion(int posicion) {
        cursor.moveToPosition(posicion);
        if (cursor.getCount()>0) return cursor.getInt(0);
        else                     return -1;s

    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int posicion) {
        Lugar lugar = lugarPosicion(posicion);
        holder.personaliza(lugar);
        holder.itemView.setTag(new Integer(posicion));
    }

    @Override public int getItemCount() {
        return cursor.getCount();
    }
} 
class AdaptadorLugaresBD(lugares: LugaresBD, var cursor: Cursor): AdaptadorLugares(lugares){

   fun lugarPosicion(posicion: Int): Lugar {
      cursor.moveToPosition(posicion)
      return (lugares as LugaresBD).extraeLugar(cursor)
   }

   fun idPosicion(posicion: Int): Int {
      cursor.moveToPosition(posicion)
      if (cursor.count>0) return cursor.getInt(0)
      else                return -1
   }

   override fun onBindViewHolder(holder: AdaptadorLugares.ViewHolder,
                                                        posicion: Int) {
      val lugar = lugarPosicion(posicion)
      holder.personaliza(lugar, onClick)
      holder.view.tag = posicion 
   }

   override fun getItemCount(): Int {
      return cursor.getCount()
   }
} 

Esta clase extiende AdaptadorLugares; de esta forma aprovechamos la mayor parte del código del adaptador y solo tenemos que indicar las diferencias. La más importante es que ahora el constructor tiene un nuevo parámetro de tipo Cursor, que es el resultado de una consulta en la base de datos. Realmente es aquí donde vamos a buscar los elementos a listar en el RecyclerView. Esta forma de trabajar es mucho más versátil que utilizar un array, podemos listar un tipo de lugar o que cumplan una determinada condición sin más que realizar un pequeño cambio en la consulta SQL. Además, podremos ordenarlos por valoración o cualquier otro criterio, y se mostrarán en el mismo orden como aparecen en el Cursor. Por otra parte, resulta muy eficiente dado que se realiza solo una consulta a la base de datos, dejando el resultado almacenado en la variable de tipo Cursor.

En Java el constructor de la clase se limita a llamar al super() y a almacenar el nuevo parámetro en una variable global. En Kotlin este proceso se indica en la declaración. En Java se han añadido los métodos getter y setter que permiten acceder al Cursor desde fuera de la clase.

Con el método lugarPosicion() vamos a poder acceder a un lugar, indicar su posición en Cursor o, lo que es lo mismo, su posición en el listado. Para ello, movemos el cursor a la posición indicada y extraemos el lugar en esta posición, utilizando un método estático de LugarBD.

Cuando queramos realizar una operación de borrado o edición de un registro de la base de datos, vamos a identificar el lugar a modificar por medio de la columna _id. Recuerda que esta columna ha sido definida en la posición 0. Para obtener el id de un lugar conociendo la posición que ocupa en el listado, se ha definido el método lugarPosicion().

Los dos últimos métodos ya existen en la clase que extendemos, pero los vamos a reemplazar; por esta razón tienen la etiqueta de override. El primero es onBindViewHolder() que se utilizaba para personalizar la vista ViewHolder en una determinada posición. La gran diferencia entre el nuevo método es que ahora el lugar lo obtenemos del cursor, mientras que en el método anterior se obtenía de lugares. Esto supondría una nueva consulta en la base de datos por cada llamada a onBindViewHolder(), lo que sería muy poco eficiente. En el método getItemCount() pasa algo similar, obtener el número de elementos directamente del cursor es más eficiente que hacer una nueva consulta.

Observa la última línea de onBindViewHolder() (holder.view.tag = posicion). El atributo Tag permite asociar a una vista cualquier objeto con información extra. La idea es asociar a cada vista del RecyclerView la posición que ocupa en el listado. Así, cuando asociamos un onClickListener este nos indica la vista pulsada, pero no la posición. De esta forma, sabiendo la vista conoceremos su posición. En la implementación anterior usábamos un método alternativo: posición = recyclerView.getChildAdapterPosition(vista). Pero tiene el inconveniente de necesitar el recyclerView. Y ahora no vamos a disponer de él.

En Kotlin tanto las clase como los atributos son por defecto cerrados . Por lo tanto, aparece un error al intentar heredar de AdaptadorLugares. Para resolverlo pulsa sobre la bombilla roja y selecciona Make AdaptadorLugares Open.

2.     Añade a la clase LugaresBD el siguiente método estático:

public static Lugar extraeLugar(Cursor cursor) {
    Lugar lugar = new Lugar();
    lugar.setNombre(cursor.getString(1));
    lugar.setDireccion(cursor.getString(2));
    lugar.setPosicion(new GeoPunto(cursor.getDouble(3),
            cursor.getDouble(4)));
    lugar.setTipo(TipoLugar.values()[cursor.getInt(5)]);
    lugar.setFoto(cursor.getString(6));
    lugar.setTelefono(cursor.getInt(7));
    lugar.setUrl(cursor.getString(8));
    lugar.setComentario(cursor.getString(9));
    lugar.setFecha(cursor.getLong(10));
    lugar.setValoracion(cursor.getFloat(11));
    return lugar;
}

public Cursor extraeCursor() {
    String consulta = "SELECT * FROM lugares";
    SQLiteDatabase bd = getReadableDatabase();
    return bd.rawQuery(consulta, null);
} 
fun extraeLugar(cursor: Cursor) = Lugar(
   nombre = cursor.getString(1),
   direccion = cursor.getString(2),
   posicion = GeoPunto(cursor.getDouble(3), cursor.getDouble(4)),
   tipoLugar = TipoLugar.values()[cursor.getInt(5)],
   foto = cursor.getString(6),
   telefono = cursor.getInt(7),
   url = cursor.getString(8),
   comentarios = cursor.getString(9),
   fecha = cursor.getLong(10),
   valoracion = cursor.getFloat(11) )

fun extraeCursor(): Cursor =
   readableDatabase.rawQuery("SELECT * FROM lugares",null) 

El primer método crea un nuevo lugar con los datos de la posición actual de un Cursor. El segundo nos devuelve un cursor que contiene todos los datos de la tabla.

3.     Abre la clase Aplicacion y reemplaza la declaración de la variable lugares y adaptador:

public RepositorioLugaresBD lugares;
public AdaptadorLugaresBD adaptador;

@Override public void onCreate() {
   super.onCreate();
   lugares = new LugaresBD(this);
   adaptador= new AdaptadorLugaresBD(lugares, lugares.extraeCursor()); 
} 
val lugares = LugaresBD(this)
val adaptador by lazy {
   AdaptadorLugaresBD(lugares, lugares.extraeCursor())} 

Hemos cambiado la declaración de lugares y adaptador para utilizar las nuevas clases. En Kotlin la inicialización de las propiedades se recomienda realizarla en su declaración. Observa como para adaptador se utiliza by lazy, para indicar que la inicialización se realice cuando vallamos a utilizar la variable. De hacerlo inmediatamente corremos el peligro de que la base de datos no esté creada.

4.     En Java, modifica las siguientes propiedades de MainActivity:

private RepositorioLugaresBD lugares;
private AdaptadorLugaresBD adaptador;
5.     Reemplaza en MainActivity dentro de onCreate() el código subrayado:
adaptador.setOnItemClickListener(new View.OnClickListener() {
   @Override public void onClick(View v) {
      int pos =(Integer)(v.getTag());
      usoLugar.mostrar(pos);
   }
}); 
adaptador.onClick = {            
   val pos = it.tag as Int
   usoLugar.mostrar(pos)
} 

6.     Ejecuta la aplicación y verifica que la listas se muestra correctamente. Si pulsas sobre un lugar se producirá un error.

Ejercicio:  Una caché para evitar accesos a los datos

Cuando la información que visualiza la aplicación se almacena en un servidor externo (puede ser en la nube o una base de datos) hay que tratar de minimizar el número de acceso al servidor. En estos casos, es frecuente almacenar en una memoria local esta información, para evitar accesos en caso de volver a necesitar la información. Esta técnica se conoce en informática como caché.

En este ejercicio vamos a crear una clase inspirada en este concepto. Realmente no crearemos una estructura de datos para implementar la caché, si no que aprovecharemos que tenemos los datos en el adaptador para no hacer nuevos accesos.

1.     Crea la clase LugaresDBAdapter con el siguiente código:

public class LugaresBDAdapter extends LugaresBD {

   private AdaptadorLugaresBD adaptador;

   public LugaresBDAdapter(Context contexto) {
      super(contexto);
   }

   public Lugar elementoPos(int pos) { 
      return adaptador.lugarPosicion (pos);
   }
}
class LugaresBDAdapter(val contexto: Context) : LugaresBD(contexto) {
   val adaptador: AdaptadorLugaresBD

   fun elementoPos(pos: Int) = adaptador.lugarPosicion(pos)   
}

Al extender de LugaresDB conseguimos que herede el comportamiento de RepositorioLugares. Añadimos el atributo adaptador que es la estructura que actuará como cache. El constructor se limita a llamar al constructor del padre. De momento solo sobrescribimos un método más, elementoPos(), que devolverá un elemento dada su posición.

Si te fijas hemos repartido el código en dos clases. En LugaresDB resolveremos el acceso a la base de datos y en LugaresDBAdapter añadimos la caché utilizando un adaptador.

2.     En Java, añade el getter y el setter para adaptador (opción Generate… > Getter and Setter).

3.     En la clase Aplicacion reemplaza la declaración de la variable lugares para que sea de la nueva clase y añade al final de onCreate():

lugares = new LugaresBDAdapter(this);
adaptador= new AdaptadorLugaresBD(lugares, lugares.extraeCursor());
lugares.setAdaptador(adaptador);
val lugares = LugaresBDAdapter(this)
…
lugares.adaptador = adaptador

4.     En Java, modifica las clases MainActivity, VistaLugarActivity, EdicionLugarActivity y CasosUsoLugar haz que lugares sea de tipo LugaresDBAdapter. En Kotlin, no es necesario, el tipo no se indica al venir de la declaración en Aplicacion.

5.     En VistaLugarActivity, EdicionLugarActivity y CasosUsosLugar modifica la siguiente línea:

lugar = lugares.elementoPos(pos);
lugar = lugares.elementoPos(pos)

Al entrar en la vista de un lugar podemos obtenerlo del adaptador a partir de su posición.

6.     Ejecuta la aplicación y verifica que funciona. Puedes seleccionar un lugar e incluso editarlo, aunque si guardas una edición no se almacenarán los cambios. Lo arreglaremos más adelante.

Ejercicio:  Ejercicio: Adaptando la actividad del Mapa a LugaresDBAdapter

En este ejercicio adaptaremos la actividad MapaActivity para que use adecuadamente la nueva forma de acceder a los lugares. Es decir, los lugares a mostrar en el mapa los obtendremos directamente del adaptador, en lugar de hacer una nueva consulta a la base de datos.

1.     En MapaActivity modifica la siguiente propiedad. Solo es necesario en Java.

private RepositorioLugaresBDAdapter lugares; 

2.     Modifica las tres llamadas a lugares.elemento() por lugares.elementoPos(). Así los lugares son extraídos del adaptador.

3.     En LugaresBDAdapter añade la siguiente función:

@Override public int tamaño(){
   return adaptador.getItemCount();
}
override fun tamaño(): Int = adaptador.itemCount 

Sobrescribimos la función para que cuando trabajemos con un LugaresBDAdapter, el número total de elementos corresponda con los que se estén listando en el RecyclerView.

4.     Verifica el funcionamiento de la actividad MapaActivity.
 

Práctica: Probando consultas en Mis Lugares

1.     En el método extraeCursor() de la clase LugaresBD reemplaza el comando SELECT * FROM lugares por SELECT * FROM lugares WHERE valoracion>1.0 ORDER BY nombre LIMIT 4. Ejecuta la aplicación y verifica la nueva lista.

2.     Realiza otras consultas similares. Si tienes dudas, puedes consultar en Internet la sintaxis del comando SQL SELECT.

3.     Si quieres practicar el uso del método query(), puedes tratar de realizar una consulta utilizando este método.
 

Práctica: Añadir criterios de ordenación y máximo en Preferencias

1.     Modifica el método extraeCursor() para que el criterio de ordenación y el máximo de lugares a mostrar corresponda con  los valores que el usuario ha indicado en las preferencias.

2.     Si el usuario escoge el primer criterio de ordenación has de dejar la consulta original sin introducir la clausula “ORDER BY”.

3.     Si escoge el orden por valoración este ha de ser descendiente, de más valorados a menos. Puedes usar la clausula “ORDER BY valoracion DESC”.

4.     Para ordenar por distancia puedes usar la siguiente consulta SQL:
 

"SELECT * FROM lugares ORDER BY " +
        "(" + lon + "-longitud)*(" + lon + "-longitud) + " +
        "(" + lat + "-latitud )*(" + lat + "-latitud )" 

Donde las variables lon y lat han de corresponder con la posición actual del dispositivo. Esta ecuación es una simplificación que no tiene en cuenta que los polos están achatados, pero funciona de forma adecuada.
 

5.     Si no actualizamos el cursor con la lista el cambio de preferencias no será efectivo hasta que salgas de aplicación y vuelvas a entrar. Para evitar este incoveniente, llama a la actividad PreferenciasActivity mediante startActivityForResult(). En el método onActivityResult() has de actualizar el cursor de adaptador e indicar que todos los elementos han de redibujarse. Para esta última acción puedes utilizar adaptador.notifyDataSetChanged().

Solución:

1.     Reemplaza en lugaresBD el siguiente método:

public Cursor extraeCursor() {
    SharedPreferences pref =
            PreferenceManager.getDefaultSharedPreferences(contexto);
    String consulta;
    switch (pref.getString("orden", "0")) {
        case "0":
            consulta = "SELECT * FROM lugares ";
            break;
        case "1":
            consulta = "SELECT * FROM lugares ORDER BY valoracion DESC";
            break;
        default:
            double lon = ((Aplicacion) contexto.getApplicationContext())
                                           .posicionActual.getLongitud();
            double lat = ((Aplicacion) contexto.getApplicationContext())
                                           .posicionActual.getLatitud();
            consulta = "SELECT * FROM lugares ORDER BY " +
                    "(" + lon + "-longitud)*(" + lon + "-longitud) + " +
                    "(" + lat + "-latitud )*(" + lat + "-latitud )";
            break;
    }
    consulta += " LIMIT "+pref.getString("maximo","12");
    SQLiteDatabase bd = getReadableDatabase();
    return bd.rawQuery(consulta, null);
} 
fun extraeCursor(): Cursor {
   val pref = PreferenceManager.getDefaultSharedPreferences(contexto)
   var consulta = when (pref.getString("orden", "0")) {
      "0" -> "SELECT * FROM lugares "
      "1" -> "SELECT * FROM lugares ORDER BY valoracion DESC"
      else -> {
         val lon = (contexto.getApplicationContext() as Aplicacion)
            .posicionActual.longitud
         val lat = (contexto.getApplicationContext() as Aplicacion)
            .posicionActual.latitud
         "SELECT * FROM lugares ORDER BY " +
                 "($lon - longitud)*($lon - longitud) + " +
                 "($lat - latitud )*($lat - latitud )"
      }
   }
   consulta += " LIMIT ${pref.getString("maximo", "12")}"
   return readableDatabase.rawQuery(consulta, null)
} 

2.     En MainActivity añade. NOTA: Si utilizas casos de uso tendrás que adaptar este código.

static final int RESULTADO_PREFERENCIAS = 0;

public void lanzarPreferencias(View view) {
   Intent i = new Intent(this, PreferenciasActivity.class);
   startActivityForResult(i, RESULTADO_PREFERENCIAS);
}

@Override protected void onActivityResult(int requestCode, int resultCode, 
                                                            Intent data) {
   super.onActivityResult(requestCode, resultCode, data);
   if (requestCode == RESULTADO_PREFERENCIAS) {
      adaptador.setCursor(lugares.extraeCursor();
      adaptador.notifyDataSetChanged();
   }
} 
val RESULTADO_PREFERENCIAS = 0

fun lanzarPreferencias(view: View? = null) = startActivityForResult(
   Intent(this, PreferenciasActivity::class.java), RESULTADO_PREFERENCIAS)

override 
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
   super.onActivityResult(requestCode, resultCode, data)
   if (requestCode == RESULTADO_PREFERENCIAS) {
      adaptador.cursor = lugares.extraeCursor()
      adaptador.notifyDataSetChanged()
   }
} 

Preguntas de repaso:  SQLite I  

Introducción unidad 8

Si has llegado hasta aquí, quiero darte la enhorabuena. Como habrás descubierto con esfuerzo, desarrollar aplicaciones en Android Nativo no es nada fácil.

Con lo que has aprendido hasta ahora tienes una base suficiente para desarrollar en este entorno. Puedes perfectamente dar el curso por concluido. No obstante, se ha añadido esta unidad extra por si quieres llegar un poco más lejos. Se tratarán temas complejos, como las bases de datos SQL, o como sincronizar información entres diferentes actividades. Si no dispones de experiencia programando es posible que parte del código te resulte complejo. Has de ir despacio, estudiando cada clase propuesta.

La aplicación desarrollada hasta este capítulo guarda la información en forma de variables. El problema de estas variables es que dejan de existir en el momento en que la aplicación es destruida. Muy frecuentemente vamos a necesitar almacenar información de manera permanente. Las alternativas más habituales para conservar esta información son los ficheros, las bases de datos o servicios a través de la red. Estas técnicas nos permiten mantener a buen recaudo los datos de la aplicación. De forma adicional, el sistema Android pone a nuestra disposición dos nuevos mecanismos para almacenar datos, las preferencias y ContentProvider.

Comenzaremos la unidad enumerando las alternativas para guardar información en Android. Luego se describirá el almacenamiento de datos usando bases de datos. Android incorpora la librería SQLite, que nos permitirá crear y manipular nuestras propias bases de datos de forma muy sencilla.

Al final de la unidad se incluye una introducción a los fragments. Se trata de elementos constructivos básicos que podremos combinar dentro del layout de una actividad. En esta edición se ha decidido no aplicarlos a Mis Lugares para no complicar en exceso los ejercicios.

Para poder seleccionar fechas y horas de forma cómoda en la aplicación Mis Lugares, aprenderemos a utilizar cuadros de dialogo con este fin.

Objetivos:

  • -Repasar las alternativas para el almacenamiento de datos en Android.
  • -Mostrar como desde Android podemos utilizar SQLite para trabajar con bases de datos.
  • -Mostrar como usando fragments podemos diseñar elementos reutilizables de la IU.
  • -Mostrar el uso de cuadros de diálogo para seleccionar fechas y horas.

Estrategias de Localización en Android

Determinar cuál es el proveedor de localización idóneo para nuestra aplicación puede resultar una tarea compleja. Además esta decisión puede variar con el tiempo según el usuario cambie de posición, o desactivar alguno de los proveedores. A continuación se plantean tres posibles estrategias:
 

Usar siempre el mismo tipo de proveedor

Los dos proveedores de localización disponibles en Android tienen características muy diferentes. Muchas aplicaciones tienen algún tipo de requisito que hace que podamos decantarnos de entrada por un sistema en concreto. Veamos algunos ejemplos.

Usaremos GPS si:

  • La aplicación requiere una precisión inferior a 10 m.
  • Está pensada para su uso al aire libre (ej. senderismo).

Usaremos localización por redes si:

  • El consumo de batería es un problema.
  • Está pensada para su uso en el interior de edificios (visita museo).

Una vez decidido, usaremos las constantes  GPS_PROVIDER  o NETWORK_PROVIDER de la clase LocationManager para indicar el proveedor deseado.

Existe un tercer tipo de proveedor identificado con la constante PASSIVE _PROVIDER. Puedes usarlo si quieres observar pasivamente actualizaciones de ubicación provocadas por otras aplicaciones, pero no quieres que se lancen nuevas lecturas de posición. De esta manera no provocamos consumo de energía adicional.
 

El mejor proveedor según un determinado criterio

Como vimos en el apartado anterior, el API de localización de Android nos proporciona la clase Criteria para seleccionar un proveedor de localización según el criterio indicado. Recordemos el código utilizado:

Criteria criterio = new Criteria();
criterio.setCostAllowed(false);
criterio.setAltitudeRequired(false);
criterio.setAccuracy(Criteria.ACCURACY_FINE);
proveedor = manejadorLoc.getBestProvider(criterio, true);

Los proveedores pueden variar de estado, por lo que podría ser interesante consultar cual es el mejor proveedor cada vez que cambie su estado.

Usar los dos proveedores en paralelo

Otra alternativa podría ser programar actualizaciones de los dos proveedores de localización disponibles. Luego podríamos seleccionar la mejor localización entre las suministradas. Para estudiar esta alternativa realiza el siguiente ejercicio:

video[Tutorial Estrategias de localizacion en Android.

Ejercicio: Añadiendo localización en Mis Lugares.

1.     Añade en AndroidManifest.xml de Mis Lugares los siguientes permisos:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

2.     Crea a la clase CasosUsoLocalizacion con los siguientes atributos y su inicialización:

public class CasosUsoLocalizacion {
   private static final String TAG = "MisLugares";
   private Activity actividad;
   private int codigoPermiso;
   private LocationManager manejadorLoc;
   private Location mejorLoc;
   private GeoPunto posicionActual; 
   private AdaptadorLugares adaptador;

   public CasosUsoLocalizacion(Activity actividad, int codigoPermiso) {
      this.actividad = actividad;
      this.codigoPermiso = codigoPermiso;
      manejadorLoc = (LocationManager) actividad.getSystemService(
      														 LOCATION_SERVICE);
      posicionActual = ((Aplicacion) actividad.getApplication())
                                                             .posicionActual;
      adaptador = ((Aplicacion) actividad.getApplication()).adaptador;
      ultimaLocalizazion();
   }
}  
class CasosUsoLocalizacion(val actividad: Activity, 
                           val codigoPermiso: Int) : LocationListener {
   val TAG = "MisLugares"
   val manejadorLoc = actividad.getSystemService(
                    AppCompatActivity.LOCATION_SERVICE) as LocationManager
   var mejorLoc: Location? = null
   val posicionActual = (actividad.application as Aplicacion).posicionActual
   val adaptador = (actividad.application as Aplicacion).adaptador

   init {
      ultimaLocalizazion()
   }
} 

La variable manejadorLoc nos permite acceder a los servicios de localización de Android. La variable mejorLoc, de tipo Location, almacena la mejor localización actual. La variable posicionActual almacena la misma información, pero en formato GeoPunto. Estará almacenada en Aplicacion para que sea accesible desde cualquier parte de la aplicación. Es necesario tener la información en dos formatos, ya que la primera variable es usada para disponer de la fecha de obtención o proveedor que nos la ha dado y la segunda al ser el formato usado en el resto de la aplicación. Finalmente obtenemos una referencia al adaptador del RecyclerView para poder actualizarlo cuando haya cambios de localización.
 

3.    En la clase Aplicacion crea la variable posicionActual:

public GeoPunto posicionActual = new GeoPunto(0.0, 0.0);  
val posicionActual = GeoPunto.SIN_POSICION.copy() 

Los valores (0, 0) representa que no se dispone de localización.

4.     En MainActivity, añade:

private static final int SOLICITUD_PERMISO_LOCALIZACION = 1;
private CasosUsoLocalizacion usoLocalizacion;


@Override protected void onCreate(Bundle savedInstanceState) {
   …
   usoLocalizacion = new CasosUsoLocalizacion(this,
                                          SOLICITUD_PERMISO_LOCALIZACION);
} 
val SOLICITUD_PERMISO_LOCALIZACION = 1
val usoLocalizacion by lazy { 
   CasosUsoLocalizacion(this, SOLICITUD_PERMISO_LOCALIZACION) }  

5.     Vamos a verificar varias veces si el usuario nos ha dado permiso de localización. Para ello añade en CasosUsoLocalizacion el siguiente código:

public boolean hayPermisoLocalizacion() {
   return (ActivityCompat.checkSelfPermission(
           actividad, Manifest.permission.ACCESS_FINE_LOCATION)
           == PackageManager.PERMISSION_GRANTED);
}  
fun hayPermisoLocalizacion() = (ActivityCompat.checkSelfPermission(
   actividad, Manifest.permission.ACCESS_FINE_LOCATION)
        == PackageManager.PERMISSION_GRANTED)  
6.    Añade el siguiente método:
 
void ultimaLocalizazion() {
  if (hayPermisoLocalizacion()) {
    if (manejadorLoc.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
         actualizaMejorLocaliz(manejadorLoc.getLastKnownLocation(
             LocationManager.GPS_PROVIDER));
    }
    if (manejadorLoc.isProviderEnabled(LocationManager.NETWORK_PROVIDER)){
         actualizaMejorLocaliz(manejadorLoc.getLastKnownLocation(
             LocationManager.NETWORK_PROVIDER));
    } 
  } else  {
         solicitarPermiso(Manifest.permission.ACCESS_FINE_LOCATION,
             "Sin el permiso localización no puedo mostrar la distancia"+
             " a los lugares.", codigoPermiso, actividad);
    
  }
} 
@SuppressLint("MissingPermission")
fun ultimaLocalizazion() {
  if (hayPermisoLocalizacion()) {
    if (manejadorLoc.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
         actualizaMejorLocaliz( manejadorLoc.getLastKnownLocation(
               LocationManager.GPS_PROVIDER))
    }
    if (manejadorLoc.isProviderEnabled(LocationManager.NETWORK_PROVIDER)){
         actualizaMejorLocaliz( manejadorLoc.getLastKnownLocation(
               LocationManager.NETWORK_PROVIDER))
    }
  } else {
         solicitarPermiso(Manifest.permission.ACCESS_FINE_LOCATION,
            "Sin el permiso localización no puedo mostrar la distancia" +
            " a los lugares.", codigoPermiso, actividad)
  }
} 
Antes de obtener una localización se debe verificar que tenemos permiso para hacerlo. Para más información consultar Permisos en Android 6 Marshmallow. En caso de tener permiso buscamos la última localización disponible. Usamos el método getLastKnownLocation() aplicado a los dos proveedores que vamos a utilizar. El método actualizaMejorLocaliz() se explicará más adelante. Si no disponemos del permiso, lo solicitamos al usuario.

7.    La función getLastKnownLocation() estará marcado con el error “Call requiered permision …”, avisándonos que hemos de comprobar que tenemos permiso antes de hacer la llamada. Realmente lo hemos hecho. Para desactivar la advertencia añade @SuppressLint("MissingPermission") antes de la función. 

8.    Copia a esta clase el método solicitarPermiso() del ejercicio Permisos en Android 6 Marshmallow.

9.     Una vez conteste el usuario se llamará a onRequestPermissionsResult de  MainActivity. Añade a la clase el siguiente método:

@Override public void onRequestPermissionsResult(int requestCode,
                               String[] permissions, int[] grantResults) {
   super.onRequestPermissionsResult(requestCode,permissions,grantResults);
   if (requestCode == SOLICITUD_PERMISO_LOCALIZACION 
       && grantResults.length == 1
       && grantResults[0] == PackageManager.PERMISSION_GRANTED) 
      usoLocalizacion.permisoConcedido();
} 
override fun onRequestPermissionsResult(requestCode: Int,
                 permissions: Array<String>, grantResults: IntArray ) {
                 super.onRequestPermissionsResult(requestCode,permissions,grantResults)
   if (requestCode == SOLICITUD_PERMISO_LOCALIZACION
       && grantResults.size == 1 
       && grantResults[0] == PackageManager.PERMISSION_GRANTED) 
     usoLocalizacion.permisoConcedido() 
} 

Si el usuario contesta afirmativamente a la solicitud de permiso llamamos a un caso de uso para que se actúe en consecuencia.

10.    Añade el siguiente caso de uso en CasosUsoLocalizacion:

public void permisoConcedido() {
   ultimaLocalizazion();
   activarProveedores();
   adaptador.notifyDataSetChanged();
}

private void activarProveedores() {
  if (hayPermisoLocalizacion()) {
    if (manejadorLoc.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
         manejadorLoc.requestLocationUpdates(LocationManager.GPS_PROVIDER,
              20 * 1000, 5, this);
    }
    if (manejadorLoc.isProviderEnabled(LocationManager.NETWORK_PROVIDER)){
              manejadorLoc.requestLocationUpdates(LocationManager
              .NETWORK_PROVIDER, 10 * 1000, 10, this);
      }
  } else {
     solicitarPermiso(Manifest.permission.ACCESS_FINE_LOCATION,
              "Sin el permiso localización no puedo mostrar la distancia"+
              " a los lugares.", codigoPermiso, actividad);
  }
} 
fun permisoConcedido() {
   ultimaLocalizazion()
   activarProveedores()
   adaptador.notifyDataSetChanged()
}
  @SuppressLint("MissingPermission")
private fun activarProveedores() {
  if (hayPermisoLocalizacion()) {
    if (manejadorLoc.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
         manejadorLoc.requestLocationUpdates(
            LocationManager.GPS_PROVIDER,20 * 1000, 5F, this )
    }
    if (manejadorLoc.isProviderEnabled(LocationManager.NETWORK_PROVIDER)){
         manejadorLoc.requestLocationUpdates(
            LocationManager.NETWORK_PROVIDER, 10 * 1000, 10F, this )
      }
  } else {
    solicitarPermiso(Manifest.permission.ACCESS_FINE_LOCATION,
         "Sin el permiso localización no puedo mostrar la distancia" +
         " a los lugares.", codigoPermiso, actividad )
  }
}  

La primera función se llama cuando nos conceden permiso de localización. Miramos si ya disponen de una última posición conocida, llamamos a la segunda función que activa los eventos de localización y refrescamos el RecyclerView.
La segunda función hace que nuestra clase (this) sea informada con cada actualización del proveedor de localización. Lo hacemos para el proveedor basado en GPS (cada 10s y si hay un cambio de más de 5m) y con el basado en redes (cada 20s y si hay un cambio de más de 10m).
 

11.    Para recibir los eventos de localización haz que la clase CasosUsoLocalizacion implemente la interfaz  LocationListener y añade las siguientes funciones:

p@Override public void onLocationChanged(Location location) {
   Log.d(TAG, "Nueva localización: "+location);
   actualizaMejorLocaliz(location);
   adaptador.notifyDataSetChanged();
}
@Override public void onProviderDisabled(String proveedor) {
   Log.d(TAG, "Se deshabilita: "+proveedor);
   activarProveedores();
}
@Override public void onProviderEnabled(String proveedor) {
   Log.d(TAG, "Se habilita: "+proveedor);
   activarProveedores();
}
@Override 
public void onStatusChanged(String proveedor, int estado, Bundle extras) {
   Log.d(TAG, "Cambia estado: "+proveedor);
   activarProveedores();
}  
override fun onLocationChanged(location: Location) {
   Log.d(TAG, "Nueva localización: $location")
   actualizaMejorLocaliz(location)
   adaptador.notifyDataSetChanged()
}
override fun onProviderDisabled(proveedor: String) {
   Log.d(TAG, "Se deshabilita: $proveedor")
   activarProveedores()
}
override fun onProviderEnabled(proveedor: String) {
   Log.d(TAG, "Se habilita: $proveedor")
   activarProveedores()
}
override fun onStatusChanged(proveedor:String, estado:Int, extras:Bundle){
   Log.d(TAG, "Cambia estado: $proveedor")
   activarProveedores()
}  

Las acciones a realizar resultan evidentes: cuando la actualizamos cambie la posición y cuando cambie el estado tratamos de activar nuevos proveedores.

12.    Ahora añade la siguiente función:

private static final long DOS_MINUTOS = 2 * 60 * 1000;

private void actualizaMejorLocaliz(Location localiz) {
   if (localiz != null && (mejorLoc == null
         || localiz.getAccuracy() < 2*mejorLoc.getAccuracy()
         || localiz.getTime() - mejorLoc.getTime() > DOS_MINUTOS)) {
      Log.d(TAG, "Nueva mejor localización");
      mejorLoc = localiz;
      posicionActual.setLatitud(localiz.getLatitude());
      posicionActual.setLongitud(localiz.getLongitude());
   }
}  
val DOS_MINUTOS:Long = (2 * 60 * 1000)

private fun actualizaMejorLocaliz(loc: Location?) {
   if (localiz != null && (mejorLoc == null
              || loc.accuracy < 2 * mejorLoc!!.getAccuracy()
              || loc.time - mejorLoc!!.getTime() > DOS_MINUTOS)) {
      Log.d(TAG, "Nueva mejor localización")
      mejorLoc = loc
      posicionActual.latitud = loc.latitude
      posicionActual.longitud = loc.longitude
   }
}  

En la variable mejorLoc almacenamos la mejor localización. Esta solo será actualizada con la nueva propuesta si: todavía no ha sido inicializada; o la nueva localización tiene una precisión aceptable (al menos la mitad que la actual); o la diferencia de tiempo es superior a dos minutos. Una vez comprobado si se cumple alguna de las tres condiciones, actualizamos mejorLocaliz y copiamos la posición en posicionActual.

13.   Si dejáramos activos los escuchadores de eventos mientras la aplicación está en segundo plano, podríamos quedarnos sin batería. Para evitar esta situación añade en MainActivity:

@Override protected void onResume() {
  super.onResume();
  usoLocalizacion.activar();
}

@Override protected void onPause() {
   super.onPause();
   usoLocalizacion.desactivar();
}  
override fun onResume() {
   super.onResume()
   usoLocalizacion.activar()
}

override fun onPause() {
   super.onPause()
   usoLocalizacion.desactivar()
}  

14.   Añade los dos nuevos casos de uso:

public void activar() {
   if (hayPermisoLocalizacion()) activarProveedores();
}

public void desactivar() {
   if (hayPermisoLocalizacion()) manejadorLoc.removeUpdates(this);
} 
fun activar() {
   if (hayPermisoLocalizacion()) activarProveedores()
}

fun desactivar() {
   if (hayPermisoLocalizacion()) manejadorLoc.removeUpdates(this)
} 

15.   Una vez que ya disponemos de la posición actual, vamos a tratar de mostrar la distancia a cada lugar en el RecyclerView de la actividad principal. Abre el layout elemento_lista.xmly añade al final del ConstraintLayout la nueva vista que se indica:

  …
<TextView
    android:id="@+id/distancia"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_toRightOf="@id/valoracion"
    android:gravity="right"
    android:text="... Km"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/direccion" />
 
</…ConstraintLayout>
 

16.    Para Java en AdaptadorLugares dentro de la clase ViewHolder la siguiente variable:

public TextView distancia; 

En el constructor de ViewHolder añade al final

distancia = itemView.distancia; 

17.   Dentro del método personaliza()añade el siguiente código al final:

GeoPunto pos=((Aplicacion) itemView.getContext().getApplicationContext())
                                                             .posicionActual;
if (pos.equals(GeoPunto.SIN_POSICION) ||
                      lugar.getPosicion().equals(GeoPunto.SIN_POSICION)) {
   distancia.setText("... Km");
} else {
   int d=(int) pos.distancia(lugar.getPosicion());
   if (d < 2000) distancia.setText(d + " m");
   else          distancia.setText(d / 1000 + " Km");
}  

val pos=(itemView.context.applicationContext as Aplicacion).posicionActual
if (pos.equals(GeoPunto.SIN_POSICION) ||
                           lugar.posicion.equals(GeoPunto.SIN_POSICION)) {
   distancia.text = "... Km"
} else {
   val d = pos.distancia(lugar.posicion).toInt()
   distancia.text = if (d < 2000) "$d m"
                    else          "${(d / 1000)} Km"

} 

Nos aseguramos de que la posición actual y la del lugar existen. Luego calculamos la distancia en la variable d. Si la distancia es inferior a 2000, se muestra en metros; en caso contrario se muestra en Km.

18.   Ejecuta la aplicación y verifica el resultado obtenido:

Nota: En este ejercicio se ha decidido extraer todo el código que nos permite mantener la localización del dispositivo a una nueva clase. Se podría haber integrado dentro de MainActivity, como se hizo en el ejercicio “La API de localización de Android”. Hacerlo de esta forma divide las responsabilidades entre las dos clase, lo que las hace más fáciles de entender y de mantener. Además el código es más reutilizable, si en un futuro queremos que otra actividad acceda a la localización, podremos usar la clase CasosUsoLocalizacion sin tener que cambiarla.   
 

Introducción unidad 6

En esta unidad comenzaremos con un aspecto de vital importancia en el desarrollo de aplicaciones en Android, el ciclo de vida de una actividad. Es decir, cómo las actividades son creadas, ejecutadas, puestas en espera y finalmente destruidas.

Continuaremos con una breve introducción sobre multimedia en Android. El API de Android viene preparado con excelentes características de reproducción multimedia, permite la reproducción de gran variedad de formatos, tanto de audio como de vídeo.

Terminamos la unidad estudiando los fundamentos del sistema de seguridad que incorpora Android. Se trata de un aspecto vital para protegernos de aplicaciones mal intencionadas que intenten violar la privacidad del usuario y evitar que realicen acciones no desadada. Gracias al sitema de permisos, se consige impedir que las aplicaciones realicen acciones comprometidas, si previamente no han solicitado el permiso adecuado.

Objetivos:

  • Comprender el ciclo de vida de una actividad Android.
  • Utilizar de forma correcta los diferentes eventos relacionados con el ciclo de vida.
  • Aprender cuándo y cómo guardar el estado de una actividad.
  • Repasar las facilidades multimedia disponibles en Android, que formatos soporta y las clases que hemos de utilizar.
  • Mostrar los pilares de la seguridad en Android
  • Describir como Android crea un usuario Linux asociado a cada aplicación.
  • Describir el esquema de permisos en Android y enumerar los permisos más importantes.

Introducción Unidad 7

En esta unidad se describe el API que incorpora Android para permitir conocer la posición geográfica del dispositivo. Estos servicios se basan principalmente en el GPS, pero también disponemos de novedosos servicios de localización basados en telefonía móvil y redes Wi-Fi. A lo largo de este capítulo mostraremos una serie de ejemplos que te permitirán aprender a utilizar estas funciones.

Terminamos la unidad describiendo como podemos incorporar a nuestra aplicación servicios realizados por terceros. En concreto instalaremos una vista que permite representar un mapa de Google Maps.

Objetivos:

  • - Describir los diferentes sistemas de posicionamiento disponibles en los dispositivos móviles actuales.

  • - Describir las APIs de Android para la geolocalización.

  • - Mostrar varias estrategias para elegir un proveedor de localización.

  • - Ver lo sencillo que resulta incorporar en nuestra aplicación un servicio de un tercero. En concreto mostraremos mapas con Google Maps.

Dialogos de selección de fecha y hora

Los cuadros de dialogo fueron introducidos en el ejercicio Un cuadro de dialogo para indicar el id de lugar. En esa ocasión aprendimos a realizar un cuadro de dialogo personalizado. En este apartado aprenderemos a utilizar cuadros de diálogo específicos para trabajar con fechas y horas.Empezaremos introduciendo algunos conceptos y clases que nos ayudarán a trabajar con este tipo de información.

Clases para trabajar con fechas en Java:

 

Clase Date[1]

 La clase Date representa un instante en el tiempo con una precisión de milisegundos. Se utiliza un sistema de medición del tiempo independiente de la zona horaria, conocido como UTC (Tiempo Universal Coordinado). El estándar de medición de tiempo UTC utiliza el tiempo en el meridiano de Greenwich independientemente de .donde nos encontremos. De esta forma, se evitan los problemas que aparecen cuando se comunican dos sistemas con mediciones locales de tiempo diferentes.

Para representar un instante de tiempo se suele utilizar la codificación conocida como “Tiempo Unix”. Esta codificación consiste en medir el número de milisegundos trascurridos desde el 1 de enero de 1970. Para almacenar este valor se utiliza un entero de 64 bits, en Java la palabra reservada long representa a un entero de este tipo. Si quieres en Android obtener el tiempo actual en este formato utiliza el método currentTimeMillis() de la clase System.

long ahora = System.currentTimeMillis();
Date fecha = new Date(ahora); 


Clase DateFormat[2]

La clase Date está pensada para contar el tiempo de forma Universal en toda la tierra, de forma que sea sencilla de manipular por una máquina. Sin embargo, las personas utilizamos una medición del tiempo que depende de la zona horaria donde estemos o incluso dependerá de si el país donde estemos utiliza el horario de verano. Cuando tengas que mostrar o solicitar una fecha a una persona, deberás utilizar la representación del tiempo a la que está acostumbrada. En este caso la clase abstracta DateFormat o su descendiente SimpleDateFormat[3] te serán de gran ayuda para este propósito.

A continuación se muestra un ejemplo sencillo:

DateFormat df = new SimpleDateFormat("dd/MM/yy");
String salida = df.format(fecha);


Clase Calendar[4]

Como hemos comentado, la clase Date utiliza internamente un simple entero para representar un instante de tiempo. Por el contrario, los humanos nos complicamos algo más dado que usamos la combinación de varios campos: como año, mes, día, hora, minuto y milisegundo. Utiliza la clase Calendar para obtener estos campos desde un objeto Date. A diferencia de la clase Date, la clase Calendar depende de la configuración local del dispositivo (locale). Para obtener, la fecha actual según la representación local del dispositivo utiliza el método getInstance():

Calendar calendario = Calendar.getInstance();
calendario.setTimeInMillis(ahora);            
int hora = calendario.get(Calendar.HOUR_OF_DAY);
int minuto = calendario.get(Calendar.MINUTE); 

La clase Calendar es una clase abstracta, que en principio te permitiría trabajar con cualquier clase de calendario (como el calendario maya o el musulmán). No obstante, el calendario usado oficialmente en casi todo el mundo es el calendario Gregoriano, definido en la clase GregorianCalendar.
 

Ejercicio: Añadiendo un dialogo de selección para cambiar la hora.
 

Un cuadro de diálogo es un tipo de ventana emergente que solicita al usuario de la aplicación algún tipo de información, antes de realizar algún proceso. Este tipo de ventanas no suele ocupar la totalidad de la pantalla. En la aplicación Mis Lugares hemos utilizado diálogos en dos ocasiones: para indicar el id a mostrar y para confirmar el borrado de un lugar. En este ejercicio aprenderemos a hacer un dialogo más complejo, que permite modificar la hora y los minutos.

1.    Abre el layout vista_lugar.xml y localiza el <imageView> que indica la hora asociada al lugar. Añade el atributo marcado:

<ImageView
    android:id="@+id/icono_hora"
    android:layout_width="40dp"
    android:layout_height="40dp"
    android:contentDescription="logo de la hora"
    android:src="@android:drawable/ic_menu_recent_history" /> 


2.    Abre la clase VistaLugarActivity y añade en el método onCreate() el siguiente código. :

findViewById(R.id.icono_hora).setOnClickListener(
   new OnClickListener() {
      public void onClick(View view) { usoLugarFecha.cambiarHora(pos); } });
findViewById(R.id.hora).setOnClickListener(
   new OnClickListener() {
      public void onClick(View view) { usoLugarFecha.cambiarHora(pos); } }); 
icono_hora.setOnClickListener { usoLugarFecha.cambiarHora(pos) }
hora.setOnClickListener { usoLugarFecha.cambiarHora(pos) } 

NOTA: Selecciona el paquete android.view.OnClickListener.
 

3.   Como acabas de ver vamos a crear un nuevo caso de uso para cambiar la hora de un lugar. La clase CasosUsoLugar empieza a ser demasiado grande. Podría ser interesante dividirla en tres partes: Operaciones de tipo CRUD (altas, bajas, modificaciones…), fotografías y de fecha y hora. De momento vamos a añadir las operaciones de fecha y hora en la clase CasosUsoLugarFecha:

public class CasosUsoLugarFecha { 

   protected AppCompatActivity actividad;
   protected LugaresBDAdapter lugares;

   public CasosUsoLugarFecha(AppCompatActivity actividad, 
                             LugaresBDAdapter lugares){
      this.actividad = actividad;
      this.lugares = lugares;
   }
} 
class CasosUsoLugarFecha(val actividad: AppCompatActivity,
                         val lugares: LugaresBDAdapter) { 
} 

4.   Añade en la nueva clase:

private int pos =-1;
private Lugar lugar;

public void cambiarHora(int pos) {
   lugar = lugares.elementoPos(pos);
   this.pos = pos;
   DialogoSelectorHora dialogo = new DialogoSelectorHora();
   dialogo.setOnTimeSetListener(this);
   Bundle args = new Bundle();
   args.putLong("fecha", lugar.getFecha());
   dialogo.setArguments(args); 
   dialogo.show(actividad.getSupportFragmentManager(), "selectorHora");
} 
var pos: Int = -1
lateinit var lugar: Lugar

fun cambiarHora(pos: Int, textView: TextView) {
   lugar = lugares.elementoPos(pos)
   this.pos = pos
   val dialogo = DialogoSelectorHora()
   dialogo.setOnTimeSetListener(this)
   val args = Bundle();
   args.putLong("fecha", lugar.fecha)
   dialogo.setArguments(args) 
   dialogo.show(actividad.supportFragmentManager, "selectorHora")
} 

Este método se ejecutará cuando se pulse sobre la hora. Su objetivo es mostrar un cuadro de diálogo para que el usuario pueda modificar la hora asociada al lugar. Los parámetros son: el pos del lugar a modificar y el TextView donde escribiremos la nueva hora. Comenzamos escribiendo dos variables donde recordaremos la información que estamos modificando, tras volver del diálogo

Continuamos creando un nuevo diálogo y luego le asignamos el escuchador a nuestra propia clase. De esta forma, cuando el usuario cambie la hora se llamará a un método de nuestra clase. Este método lo crearemos en uno de los puntos siguientes. A este diálogo le pasamos como argumento la fecha del lugar en un long. Finalmente, mostramos el diálogo llamando al método show(). Este método utiliza dos parámetros: el manejador de fragments y una etiqueta que identificará el cuadro de diálogo.
 

5.    Crea la siguiente clase en el paquete presentacion:

public class DialogoSelectorHora extends DialogFragment {

    private OnTimeSetListener escuchador;

    public void setOnTimeSetListener(OnTimeSetListener escuchador) {
        this.escuchador = escuchador;
    }
    
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        Calendar calendario = Calendar.getInstance();
        Bundle args = this.getArguments();
        if (args != null) {
            long fecha = args.getLong("fecha");
            calendario.setTimeInMillis(fecha);
        }
        int hora = calendario.get(Calendar.HOUR_OF_DAY);
        int minuto = calendario.get(Calendar.MINUTE);
        return new TimePickerDialog(getActivity(), escuchador, hora, 
                  minuto, DateFormat.is24HourFormat(getActivity()));
    }
 } 
class DialogoSelectorHora : DialogFragment() {

   private var escuchador: OnTimeSetListener? = null

   fun setOnTimeSetListener(escuchador: OnTimeSetListener) {
      this.escuchador = escuchador
   }

   override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
      val calendario = Calendar.getInstance()
      val fecha = arguments?.getLong("fecha")?:System.currentTimeMillis()
      calendario.setTimeInMillis(fecha)
      val hora = calendario.get(Calendar.HOUR_OF_DAY)
      val minuto = calendario.get(Calendar.MINUTE)
      return TimePickerDialog(
         getActivity(), escuchador, hora,
         minuto, DateFormat.is24HourFormat(getActivity())
      )
   }
} 

NOTA: Pulsa Alt-Intro para añadir los imports automáticamente. Algunas clases se encuentran en varios paquetes, por lo que te preguntará. Utiliza: androidx.fragment.app.DialogFragment y android.text.format.DateFormat

Esta clase extiende DialogFragment, que define un fragment que muestra una ventana de diálogo flotante sobre la actividad. El control del cuadro de diálogo debe hacerse siempre a través de los métodos del API, nunca directamente.

Para definir un nuevo DialogFragment se puede sobreescribir onCreateView() para indicar el contenido del diálogo. Alternativamente, se puede sobreescribir onCreateDialog() para crear un diálogo totalmente personalizado, como hacemos en este ejercicio. En este método hay que devolver un objeto Dialog que se  mostrará.

Creamos un objeto Calendar y si nos han pasado una fecha se la asignamos. En caso contrario, la fecha corresponderá con la actual. Luego extraemos la hora y los minutos del calendario.

Finalmente creamos un nuevo dialogo de la clase TimePickerDialog. Se trata de un tipo de dialogo definido en el sistema que nos permite seleccionar horas y minutos. En su constructor indicamos cuatro parámetros: el contexto, un escuchador al que llamará cuando se seleccione la hora, la hora y minutos que se mostrarán al inicio y un valor booleano que indica si trabajamos con formato de 24 horas o de 12. En el código se usa el valor definido en nuestro contexto.
 

6.    Haz que CasosUsoLugarFecha implemente la siguiente interfaz:

public class CasosUsoLugarFecha implements TimePickerDialog.OnTimeSetListener { 
class CasosUsoLugarFecha(…) : TimePickerDialog.OnTimeSetListener { 


7.    Añade la siguiente función:

@Override public void onTimeSet(TimePicker vista, int hora, int minuto) {
   Calendar calendario = Calendar.getInstance();
   calendario.setTimeInMillis(lugar.getFecha());
   calendario.set(Calendar.HOUR_OF_DAY, hora);
   calendario.set(Calendar.MINUTE, minuto);        
   lugar.setFecha(calendario.getTimeInMillis());
   lugares.actualizaPosLugar(pos, lugar);
   TextView textView = actividad.findViewById(R.id.hora);        
   textView.setText(DateFormat.getTimeInstance().format(
                                            new Date(lugar.getFecha())));
} 
override fun onTimeSet(vista: TimePicker?, hora: Int, minuto: Int) {
   val calendario = Calendar.getInstance()
   calendario.setTimeInMillis(lugar.fecha)
   calendario.set(Calendar.HOUR_OF_DAY, hora)
   calendario.set(Calendar.MINUTE, minuto)
   lugar.fecha = calendario.getTimeInMillis()
   lugares.actualizaPosLugar(pos, lugar)
   val textView = actividad.findViewById<TextView>(R.id.hora)
   textView.text= DateFormat.getTimeInstance().format(Date(lugar.fecha))
} 

En el punto anterior hemos indicado que nuestra clase actuaría como escuchador, cuando se seleccionara una hora en el cuadro de diálogo. Como consecuencia este método será llamado. Se nos pasan tres parámetos. En este caso nos interesa la hora y los minutos seleccionados. Para cambiar esta información en la fecha asociada al lugar, comenzamos creando un objeto Calendar y lo inicializamos con la fecha que tiene el lugar. Luego, le modificamos la hora y los minutos según los parámetros que nos han indicado. Hay que aclarar que el resto de la fecha, como el día o el mes, no van a modifcarse. La nueva fecha es introducida en el objeto lugar y a continuación actualizamos la base de datos.

Para modificar el TextView de la hora, comenzamos creando un formato de fecha, donde se visualizará la hora y los minutos separado por dos puntos. Para convertir la fecha correctamente hay que conocer la zona horaria definida en el sistema. Esto se consigue con java.util.Locale.getDefault(). Finalmente usamos este formato sobre un objeto Date para cambiara el contenido del TextView.   

8.    En VistaLugarActivity crea la variable usoLugarFecha de la clase CasosUsoLugarFecha de igual forma como se ha creado usoLugar.

9.    Ejecuta la aplicación y verifica el resultado.

 

Práctica: Añadiendo un dialogo de selección para cambiar la fecha.
 

Podrías crear un cuadro de dialogo para modificar la fecha asociada al lugar (día, mes y año). Has de realizar los mismos pasos que en el ejecio anterior, pero ahora se basará en el diálogo siguiente.

En este caso tendrás que usar un diálogo de la clase DatePickerDialog:

DatePickerDialog(actividad , escuchador, año, mes, dia); 

El escuchador ha de implentar el interface OnDateSetListener y este interface define el siguiente método:

@Override
public void onDateSet(DatePicker view, int anyo, int mes, int dia) {…} 

Finalmente utiliza el siguiente formato para representarlo:

DateFormat formato = DateFormat.getDateInstance(); 

En este caso no se define un formato concreto como en el ejercicio anterior, si no que se selecciona el definido en el sistema para representar una fecha. De esta forma, el formato será el que ha configurado el usuario en el dispositivo.
 

Solución:
 

Añade en VistaLugarActivity dentro de onCreate():

findViewById(R.id.icono_fecha).setOnClickListener(
   new OnClickListener() {
      public void onClick(View view) { usoLugarFecha.cambiarFecha(pos); } });
findViewById(R.id.fecha).setOnClickListener(
   new OnClickListener() {
      public void onClick(View view) { usoLugarFecha.cambiarFecha(pos); }
icono_fecha.setOnClickListener { usoLugarFecha.cambiarFecha(pos) }
fecha.setOnClickListener { usoLugarFecha.cambiarFecha(pos) } 

En la clase CasosUsoLugarFecha añade la interfaz y las dos funciones indicadas.

public class CasosUsoLugarFecha implements
  TimePickerDialog.OnTimeSetListener , DatePickerDialog.OnDateSetListener {

public void cambiarFecha(int pos) {
   lugar = lugares.elementoPos(pos);
   this.pos = pos;
   DialogoSelectorFecha dialogo = new DialogoSelectorFecha();
   dialogo.setOnDateSetListener(this);
   Bundle args = new Bundle();
   args.putLong("fecha", lugar.getFecha());
   dialogo.setArguments(args);
   dialogo.show(actividad.getSupportFragmentManager(),"selectorFecha");
}

@Override
public void onDateSet(DatePicker view, int año, int mes, int dia) {
   Calendar calendario = Calendar.getInstance();
   calendario.setTimeInMillis(lugar.getFecha());
   calendario.set(Calendar.YEAR, año);
   calendario.set(Calendar.MONTH, mes);
   calendario.set(Calendar.DAY_OF_MONTH, dia);
   lugar.setFecha(calendario.getTimeInMillis());
   lugares.actualizaPosLugar(pos, lugar); 
   TextView textView = actividad.findViewById(R.id.fecha);    
   textView.setText(DateFormat.getDateInstance().format(
                                             new Date(lugar.getFecha())));
} 
class CasosUsoLugarFecha(…) : 
   TimePickerDialog.OnTimeSetListener, DatePickerDialog.OnDateSetListener{

fun cambiarFecha(pos: Int) {
   lugar = lugares.elementoPos(pos)
   this.pos = pos
   val dialogo = DialogoSelectorFecha()
   dialogo.setOnDateSetListener(this)
   val args = Bundle()
   args.putLong("fecha", lugar.fecha)
   dialogo.setArguments(args)
   dialogo.show(actividad.supportFragmentManager, "selectorFecha")
}

override fun onDateSet(view: DatePicker, año: Int, mes: Int, dia: Int) {
   val calendario = Calendar.getInstance()
   calendario.timeInMillis = lugar.fecha
   calendario.set(Calendar.YEAR, año)
   calendario.set(Calendar.MONTH, mes)
   calendario.set(Calendar.DAY_OF_MONTH, dia)
   lugar.fecha = calendario.timeInMillis
   lugares.actualizaPosLugar(pos, lugar)
   val textView = actividad.findViewById<TextView>(R.id.fecha)
   textView.text = java.text.DateFormat.getDateInstance().format(Date(lugar.fecha))
} 
Crea la clase DialogoSelectorFecha:
 
public class DialogoSelectorFecha extends DialogFragment {

   private OnDateSetListener escuchador;

   public void setOnDateSetListener(OnDateSetListener escuchador) {
      this.escuchador = escuchador;
   }
   @Override public Dialog onCreateDialog(Bundle savedInstanceState) {
      Calendar calendario = Calendar.getInstance();
      Bundle args = this.getArguments();
      if (args != null) {
         long fecha = args.getLong("fecha");
         calendario.setTimeInMillis(fecha);
      }
      int año = calendario.get(Calendar.YEAR);
      int mes = calendario.get(Calendar.MONTH);
      int dia = calendario.get(Calendar.DAY_OF_MONTH);
      return new DatePickerDialog(getActivity(),escuchador,año,mes,dia);
   }
} 
class DialogoSelectorFecha : DialogFragment() {

   private var escuchador: OnDateSetListener? = null

   fun setOnDateSetListener(escuchador: OnDateSetListener) {
      this.escuchador = escuchador
   }

   override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
      val calendario = Calendar.getInstance()
      val fecha = getArguments()?.getLong("fecha")?:0L
      calendario.setTimeInMillis(fecha)
      val año = calendario.get(Calendar.YEAR)
      val mes = calendario.get(Calendar.MONTH)
      val dia = calendario.get(Calendar.DAY_OF_MONTH)
      return DatePickerDialog(getActivity(), escuchador, año, mes, dia)
   }
} 

Añadiendo fotografías en Mis Lugares

En este apartado seguiremos trabajando con el uso de intenciones aplicándolas a la aplicación Mis Lugares. En concreto permitiremos que el usuario pueda asignar fotografías a cada lugar utilizando ficheros almacenados en su dispositivo o la cámara.

Ejercicio: Añadiendo fotografías desde la galería

1.     En la clase VistaLugarActivity añade las siguientes constantes y atributos:

final static int RESULTADO_GALERIA = 2;
final static int RESULTADO_FOTO = 3;
private ImageView foto;  
val RESULTADO_GALERIA = 2  //poner antes de la clase
val RESULTADO_FOTO = 3 

Desde la actividad VistaLugarActivity llamamos a diferentes actividades y algunas de ellas nos tienen que devolver información. En estos casos llamamos a la actividad con startActivityForResult() pasándole un código que identifica la llamada. Cuanto esta actividad termine se llamará al método  onActivityResult(), que nos indicará el mismo código usado en la llamada. Como vamos a hacerlo con  tres actividades diferentes, hemos creado tres constantes, con los respectivos códigos de respuesta. Actuando de esta forma conseguimos un código más legible.

2.     En Java, en el método onCreate() añade antes de actualizarVistas():

foto = findViewById(R.id.foto);

3.     Busca en vista_lugar.xml el ImageView con descripción “logo galería” y añade el atributo:
 

android:onClick="ponerDeGaleria" 

4.     Añade la siguiente función en la actividad:

public void ponerDeGaleria(View view) {
   usoLugar.ponerDeGaleria(RESULTADO_GALERIA);
} 
fun ponerDeGaleria(view: View)= usoLugar.ponerDeGaleria(RESULTADO_GALERIA) 

5.     Añade el siguiente caso de uso a la clase CasosUsoLugar:

// FOTOGRAFÍAS
public void ponerDeGaleria(int codidoSolicitud) {
    String action;
    if (android.os.Build.VERSION.SDK_INT >= 19) { // API 19 - Kitkat
        action = Intent.ACTION_OPEN_DOCUMENT;
    } else {
        action = Intent.ACTION_PICK;
    }
    Intent intent = new Intent(action, 
                           MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    intent.setType("image/*");
    actividad.startActivityForResult(intent, RESULTADO_GALERIA);
} 
// FOTOGRAFÍAS
fun galeria(view: View) {
   val action= if (android.os.Build.VERSION.SDK_INT >= 19) { // API 19 - Kitkat
      Intent.ACTION_OPEN_DOCUMENT
   } else {
      Intent.ACTION_PICK
   }
   val i = Intent(action,MediaStore.Images.Media.EXTERNAL_CONTENT_URI).apply {
      addCategory(Intent.CATEGORY_OPENABLE)
      type = "image/*"
   }
   startActivityForResult(i, RESULTADO_GALERIA)
} 

Este método crea una intención indicando que queremos seleccionar contenido. El contendo será proporcionado por el Content Provider MediaStore, además le indicamos que nos interesan imágenes del almacenamiento externo. Tipicamente se abrirá la aplicación galería de fotos (u otra similar).  Observa, que se usan dos acciones diferentes: ACTION_OPEN_DOCUMENT solo está disponible a partir del API 19, tiene la ventaja que no requiere que la aplicación pida permiso de lectura. Cuando el usuario selecciona un fichero el Content Provider, dará a nuestra aplicación permiso de lectura (o incluso de escritura) pero solo para el archivo solicitado. No se considera una acción peligrosa dado que es el usuario quien selecciona el archivo a compartir con la aplicación. Si se ejecuta en un API anterior al 19, tendremos que usar ACTION_PICK, que sí que requiere dar permisos de lectura en la memoria externa. Una vez concedido este permiso la aplicación podría aprovechar y leer otros ficheros sin la intervención directa del usuario. Como necesitamos una respuesta usamos startActivityForResult() con el código adecuado.

6.    Si la versión  del dispositivo es anterior al API 19, vamos a tener que pedir permiso para leer ficheros de la memoria externa. En AndroidManifest.xml añade dentro de la etiqueta <manifest …> </manifest> el siguiente código:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> 

7.     En el método onActivityResult() añade la sección else if que se muestra:

if (requestCode == RESULTADO_EDITAR) {
   …
} else if (requestCode == RESULTADO_GALERIA) {
   if (resultCode == Activity.RESULT_OK) {
      usoLugar.ponerFoto(pos, data.getDataString(), foto);
   } else {
      Toast.makeText(this, "Foto no cargada",Toast.LENGTH_LONG).show();
   }
} 
if (requestCode == RESULTADO_EDITAR) {
   …
} else if (requestCode == RESULTADO_GALERIA) {
   if (resultCode == RESULT_OK) {
      usoLugar.ponerFoto(pos, data?.dataString ?: "", foto)
   } else {
      Toast.makeText(this, "Foto no cargada", Toast.LENGTH_LONG).show();
   }
}  

Comenzamos verificando que volvemos de la actividad lanzada por la intención anterior. Comprobamos que el usuario no ha cancelado la operación. En este caso, nos tiene que haber pasado en la intención de respuesta, data, una URI con la foto seleccionada. Esta URI puede ser del tipo file://…, content://…o http://… dependiendo de que aplicación haya resuelto esta intención. El siguiente paso consiste en modificar el contenido de la vista que muestra la foto, imageView, con esta URI. Lo hacemos en el método ponerFoto():

8.     Añade el siguiente CasosUsoLugar:

public void ponerFoto(int pos, String uri, ImageView imageView) {
   Lugar lugar = lugares.elemento(pos);  
   lugar.setFoto(uri);
   visualizarFoto(lugar, imageView);
}

public void visualizarFoto(Lugar lugar, ImageView imageView) {
   if (lugar.getFoto() != null && !lugar.getFoto().isEmpty()) {
      imageView.setImageURI(Uri.parse(lugar.getFoto()));
   } else {
      imageView.setImageBitmap(null);
   }
}  
fun ponerFoto(pos: Int, uri: String?, imageView: ImageView) {
   val lugar = lugares.elemento(pos)
   lugar.foto = uri ?: ""
   visualizarFoto(lugar, imageView)
}

fun visualizarFoto(lugar: Lugar, imageView: ImageView) {
   if (!(lugar.foto == null || lugar.foto.isEmpty())) {
      imageView.setImageURI(Uri.parse(uri))
   } else {
      imageView.setImageBitmap(null)
   }
} 

El primer método comienza obteniendo el lugar que corresponde al id para modificar la URI de su foto. Luego llama a visualizarFoto(). Este verifica que la URI que acabamos de asignar no está vacía. Si es así, la asigna al ImageView que nos han pasado para que la imagen se represente en pantalla. En caso contrario, se le asigna un Bitmap igual a null, que es equivalente a que no se represente ninguna imagen.

9.     Ya puedes ejecutar la aplicación. Si añades una fotografía a un lugar, esta se visualizará. Sin embargo, si vuelve a la lista de lugares y seleccionas el mismo lugar al que asignaste la fotografía, ésta ya no se representa. La razón es que no hemos visualizado la foto al crear la actividad.

10.  En el método actualizarVistas() añade la siguiente línea al final:

usoLugar.visualizarFoto(lugar, foto); 
11.  Verifica de nuevo el funcionamiento de la aplicación.
 

Ejercicio paso a paso: Añadiendo fotografías desde la cámara

1.     Añade los siguientes atributos a la clase VistaLugarActivity:

private Uri uriUltimaFoto; 
private lateinit var uriUltimaFoto: Uri 

Como veremos, necesitamos esta variable en dos métodos diferentes. Por lo tanto, la declaramos de forma global.

2.     Añade el siguiente método a la clase CasosUsoLugar:

public Uri tomarFoto(int codidoSolicitud) {
   try {
      Uri uriUltimaFoto;
      File file = File.createTempFile(
           "img_" + (System.currentTimeMillis()/ 1000), ".jpg" ,
           actividad.getExternalFilesDir(Environment.DIRECTORY_PICTURES));
      if (Build.VERSION.SDK_INT >= 24) {
         uriUltimaFoto = FileProvider.getUriForFile(
               actividad, "es.upv.jtomas.mislugares.fileProvider", file); 
      } else {
         uriUltimaFoto = Uri.fromFile(file);
      }
      Intent intent   = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
      intent.putExtra (MediaStore.EXTRA_OUTPUT, uriUltimaFoto);
      actividad.startActivityForResult(intent, codidoSolicitud);
      return uriUltimaFoto;
   } catch (IOException ex) {
      Toast.makeText(actividad, "Error al crear fichero de imagen", 
            Toast.LENGTH_LONG).show();
      return null;
   }
}  
fun tomarFoto(codidoSolicitud: Int): Uri? {
   try {
      val file = File.createTempFile(
         "img_" + System.currentTimeMillis() / 1000, ".jpg", 
         actividad.getExternalFilesDir(Environment.DIRECTORY_PICTURES) )
      val uriUltimaFoto = if (Build.VERSION.SDK_INT >= 24)
         FileProvider.getUriForFile(
            actividad, "es.upv.jtomas.mislugares.fileProvider", file )
      else Uri.fromFile(file)
      val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
      intent.putExtra(MediaStore.EXTRA_OUTPUT, uriUltimaFoto)
      actividad.startActivityForResult(intent, codidoSolicitud)
      return uriUltimaFoto
   } catch (ex: IOException) {
      Toast.makeText(actividad, "Error al crear fichero de imagen",
                     Toast.LENGTH_LONG).show()
      return null
   }
}  

Este método crea  una intención indicando que queremos capturar una imagen desde el dispositivo. Típicamente se abrirá la aplicación cámara de fotos. A esta intención vamos a añadirle un extra con una URI al fichero donde queremos que se almacene la fotografía. Para crear el fichero, se utiliza createTempFile() indicando nombre, extensión y directorio. El método currentTimeMillis() nos da el número de milisegundos transcurridos desde 1970. Al dividir entre 1000, tenemos el número de segundos. El objetivo que se persigue es que, al crear un nuevo fichero, su nombre nunca coincida con uno anterior. El directorio del fichero será el utilizado para almacenar fotos privadas en la memoria externa. Estos ficheros serán de uso exclusivo para tu aplicación. Además, si desístalas la aplicación este directorio será borrado. Si quieres que los ficheros sean de acceso público utiliza  getExternalStoragePublicDirectory().
 Una vez tenemos el fichero, hay dos alternativas para crear la URI. Si el API de Android donde se ejecuta la aplicación es 24 o superior, podemos crear el fichero asociado a un Content Provider nuestro. Esta acción no requiere solicitar permiso de escritura. Si el API es anterior al 24, no se dispone de esta acción, y el fichero será creado de forma convencional en la memoria externa. El inconveniente es que para realizar esta acción tendremos que pedir al usuario permiso de escritura en la memoria externa. Al concedernos este permiso, también podremos borrar o sobreescribir cualquier fichero que el usuario tenga en esta memoria. Por lo que muchos usuarios no querrán darnos este permiso. Finalmente se añade la extensión del fichero. Al final llamamos a startActivityForResult() con el código que nos han pasado.

3.     Si la versión mínima de API es anterior a 24, vamos a tener que pedir permiso para leer ficheros de la memoria externa. En AndroidManifest.xml añade dentro de la etiqueta <manifest …> </manifest> el siguiente código:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> 

NOTA: Observa como en el ejercicio anterior hemos tenido que solicitar permiso para acceder a la memoria externa, pero en este no es necesario solicitar permiso para tomar una fotografía. La razón es que realmente nuestra aplicación no toma la fotografía directamente, si no que por medio de una intención lanzamos otra aplicación, que si que tiene este permiso.

4.     En un punto anterior hemos utilizado un Content Provider para almacenar ficheros. Para crearlo añade en AndroidManifest.xml dentro de la etiqueta <application …> </application> el siguiente código:

<provider
   android:name="androidx.core.content.FileProvider"
   android:authorities="es.upv.jtomas.mislugares.fileProvider"
   android:exported="false"
   android:grantUriPermissions="true">
   <meta-data  android:name="android.support.FILE_PROVIDER_PATHS"
               android:resource="@xml/file_paths" />
</provider>  

5.     Crea un nuevo recurso con nombre "file_paths.xml" en la carpeta res/ xml:

<paths>
   <external-files-path name="my_images" path="/" />
</paths> 

Has de cambiar es.upv.jtomas… por un identificador unido que incluya tu nombre o empresa y la aplicación. Ha de coincidir con el valor indicado en la función tomarFoto(). Cambia com.example.package.name por el paquete de la aplicación.

6.     Busca en vista_lugar.xml el ImageView con descripción “logo cámara” y añade el atributo:
android:onClick="tomarFoto"

7.     En VistaLugarActivity añade:

public void tomarFoto(View view) {
   uriUltimaFoto = usoLugar.tomarFoto(RESULTADO_FOTO) 
} 
fun tomarFoto(view: View) {
   uriUltimaFoto = usoLugar.tomarFoto(RESULTADO_FOTO) 
} 
8.     En el método onActivityResult() añade la sección else if que se muestra:
} else if (requestCode == RESULTADO_GALERIA
   …
} else if (requestCode == RESULTADO_FOTO) {
   if (resultCode == Activity.RESULT_OK && uriUltimaFoto!=null) {
      lugar.setFoto(uriUltimaFoto.toString());
      usoLugar.ponerFoto(pos, lugar.getFoto(), foto);
   } else {
      Toast.makeText(this, "Error en captura", Toast.LENGTH_LONG).show();
   }
} 
} else if (requestCode == RESULTADO_GALERIA) {
   … 
} else if (requestCode == RESULTADO_FOTO) {
   if (resultCode == Activity.RESULT_OK && uriUltimaFoto!=null) {
      lugar.foto = uriUltimaFoto.toString()
      usoLugar.ponerFoto(pos, lugar.foto, foto);
   } else {
      Toast.makeText(this, "Error en captura", Toast.LENGTH_LONG).show()
   }
} 

Comenzamos verificando que volvemos de la actividad lanzada por la intención anterior, que el usuario no ha cancelado la operación y que uriUltimaFoto ha sido inicializado. En este caso, se nos pasa información en la intención de respuesta, pero sabemos que en uriUltimaFoto está almacenada la URI con el fichero donde se ha almacenado la foto. Guardamos esta URI en el campo adecuado de lugar e indicamos que se guarde y se represente en la vista foto.
 

9.     Verifica de nuevo el funcionamiento de la aplicación.

NOTA: En algunos dispositivos puede aparecer un error de memoria si la cámara está configurada con mucha resolución. En estos casos puedes probar con la cámara delantera.
 

Ejercicio paso a paso: Añadiendo un botón para eliminar fotografías

1.     En el layout vista_lugar.xml añade el siguiente botón dentro del LinearLayout donde están los botones para la cámara y para galería:

<ImageView
    android:layout_width="40dp"
    android:layout_height="40dp"
    android:contentDescription="Eliminar foto"
    android:onClick="eliminarFoto"
    android:src="@android:drawable/ic_menu_close_clear_cancel" /> 
2.     Añade el siguiente método a la clase VistaLugarActivity:
public void eliminarFoto(View view) {
   usoLugar.ponerFoto(pos, "", foto); 
}  
fun eliminarFoto(view: View) = usoLugar.ponerFoto(pos, "", foto)  

3.     Verifica el funcionamiento del nuevo botón.

NOTA: Las fotografías introducidas por el usuario pueden tener muy diversas procedencias, pudiendo tener grandes tamaños. Cuando trabajas con fotografías es muy importante que tengas en cuenta que la memoria es un recurso limitado. Por lo tanto, es muy probable que cuando trates de cargar una imagen demasiado grande, tu aplicación se detenga, mostrando en el LogCat un error de memoria. Para resolver el problema se podría cargar la imagen a una resolución menor, adecuada para un dispositivo móvil. Para ello puedes utilizar una librería de carga de imágenes como Glide o Picasso. También puedes realizar este proceso siguiendo esta documentación: https://developer.android.com/topic/performance/graphics/load-bitmap

Práctica: Confirmar borrado fotografías

1.     Si lo deseas, puedes poner un cuadro de diálogo para confirmar la eliminación. Puedes basarte en la práctica «Un cuadro de diálogo para confirmar el borrado».

Uso de intenciones en Mis Lugares

Ejercicio: Intenciones implícitas en Mis Lugares

1. Si has hecho el ejercicio Casos de Uso para lugares, en  la clase CasosUsoLugar añade cuatro nuevos casos de uso: Si no añádelos a la clase VistaLugarActivity.

// INTENCIONES
public void compartir(Lugar lugar) {
   Intent i = new Intent(Intent.ACTION_SEND);
   i.setType("text/plain");
   i.putExtra(Intent.EXTRA_TEXT, 
              lugar.getNombre() + " - " + lugar.getUrl());
   actividad.startActivity(i);
}

public void llamarTelefono(Lugar lugar) {
   actividad.startActivity(new Intent(Intent.ACTION_DIAL, 
            Uri.parse("tel:" + lugar.getTelefono())));
}

public void verPgWeb(Lugar lugar) {
   actividad.startActivity(new Intent(Intent.ACTION_VIEW,
            Uri.parse(lugar.getUrl())));
}

public final void verMapa(Lugar lugar) {
   double lat = lugar.getPosicion().getLatitud();
   double lon = lugar.getPosicion().getLongitud();
   Uri uri = lugar.getPosicion() != GeoPunto.SIN_POSICION
      ? Uri.parse("geo:" + lat + ',' + lon) 
      : Uri.parse("geo:0,0?q=" + lugar.getDireccion());
   actividad.startActivity(new Intent("android.intent.action.VIEW", uri));
} 
// INTENCIONES
fun compartir(lugar: Lugar) = actividad.startActivity(
   Intent(Intent.ACTION_SEND).apply {
      type = "text/plain"
      putExtra(Intent.EXTRA_TEXT, "$(lugar.nombre) - $(lugar.url)")
   })

fun llamarTelefono(lugar: Lugar) = actividad.startActivity(
   Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + lugar.telefono)))

fun verPgWeb(lugar: Lugar) = actividad.startActivity(
   Intent(Intent.ACTION_VIEW, Uri.parse(lugar.url)))

fun verMapa(lugar: Lugar) {
   val lat = lugar.posicion.latitud
   val lon = lugar.posicion.longitud
   val uri = if (lugar.posicion != GeoPunto.SIN_POSICION)
                  Uri.parse("geo:$lat,$lon")
             else Uri.parse("geo:0,0?q=" + lugar.direccion)
   actividad.startActivity(Intent(Intent.ACTION_VIEW, uri))
} 
El primer método crea una intención implícita con acción ACTION_SEND. Mandaremos un texto plano formado por el nombre del lugar y su URL. Esta información podrá ser recogida por cualquier aplicación que se haya registrado como enviadora de mensajes (WhatsApp, Gmail, SMS, etc.).

El segundo método realiza una llamada telefónica al número del lugar. El tercero abre su página Web. El cuarto obtiene la latitud y longitud del lugar. Si alguna de las dos es distinta de cero, consideraremos que se ha introducido esta información y crearemos una URI basada en estos valores. Si son cero, consideraremos que no se han introducido, por lo que crearemos una URI basándonos en la dirección del lugar.

2.   En la clase VistaLugarActivity hay que llamar a dos de estos métodos desde el menú. En el método onOptionsItemSelected() añade el código subrayado:

@Override public boolean onOptionsItemSelected(MenuItem item) {
   switch (item.getItemId()) {
   case R.id.accion_compartir:
      usoLugar.compartir(lugar);
      return true;
   case R.id.accion_llegar:
      usoLugar.verMapa(lugar);
      return true;
      … 
override fun onOptionsItemSelected(item: MenuItem): Boolean {
   when (item.getItemId()) {
      R.id.accion_compartir -> {
         usoLugar.compartir(lugar)
         return true
      }
      R.id.accion_llegar -> {
         usoLugar.verMapa(lugar)
         return true
      }
      … 

NOTA: Si no has hecho el ejercicio Casos de Uso para lugares, elimina usoLugar.  

3.    Ejecuta la aplicación, selecciona alguno de los lugares y utiliza la barra de acciones para verificar estas opciones. Para verificar la segunda opción, es importante que el terminal disponga de algún software de mapas (como Google Maps).

4.    Abre el layout vista_lugar.xml. Localiza el LinearLayout que contiene el icono y el texto con la dirección. Añade el atributo onClick como se muestra a continuación:

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal" 
    android:onClick="verMapa">
        <ImageView
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:contentDescription="logo de la dirección"
            android:src="@android:drawable/ic_menu_myplaces" />
        … 

5.  Añade también el atributo onClick en los LinearLayout que contienen el teléfono y la URL utilizando el método adecuado.

6.  Añade a VistaLugarActivity los siguientes métodos:

public void verMapa(View view) {
   usoLugar.verMapa(lugar);
}

public void llamarTelefono(View view) {
   usoLugar.llamarTelefono(lugar);
}

public void verPgWeb(View view) {
   usoLugar.verPgWeb(lugar);
} 
fun verMapa(view: View) = usoLugar.verMapa(lugar)

fun llamarTelefono(view: View) = usoLugar.llamarTelefono(lugar)

fun verPgWeb(view: View) = usoLugar.verPgWeb(lugar) 


NOTA: Si no has hecho el ejercicio Casos de Uso para lugares, elimina usoLugar.  

7.  Verifica el funcionamiento de las nuevas intenciones implícitas.

Introducción unidad 3

En esta unidad seguimos trabajando con el diseño del interfaz de usuario. En lugar de tratar aspectos de diseño visual, como hicimos en la unidad anterior, vamos a centrarnos en temas más relacionados con el código. En concreto nos centraremos en las Actividades. Estudiaremos también dos herramientas de gran utilidad para cualquier aplicación: la barra de acciones y la definición de las preferencias de configuración. 

Nos vamos a centrar en el ejemplo de aplicación que estamos desarrollando, Mis Lugares, para añadirle diferentes actividades.  

A medida que crece el tamaño de una aplicación, resulta más complicado mantener el código ordenado y sin errores. Es aquí, donde el uso de correctas arquitecturas de software puede ayudarnos. En este capítulo vamos a realizar una breve introducción de estos conceptos. En concreto, veremos cómo almacenar información que pueda ser accesible desde toda la aplicación y la arquitectura Clean. El objetivo principal no es adquirir unos conocimientos profundos en esta materia, sino más bien, que un programador novel asuma la importancia de utilizar estas técnicas utilizándolas en la práctica.

Objetivos:

  • Describir cómo el interfaz de usuario en una aplicación Android estará formado por un conjunto de actividades.
  • Mostrar cómo desde una actividad podemos invocar a otras y cómo podemos comunicarnos con ellas.
  • Incorporar a nuestras aplicaciones ciertos elementos prácticos como son los menús o las preferencias.
  • Introducir conceptos de arquitectura de software, como el patrón Singleton y la arquitectura Clean.
  • Describir cómo podemos utilizar y crear iconos en nuestras aplicaciones.

Añadiendo preferencias de usuario

Android nos facilita la configuración de nuestros programas, al permitir añadir una lista de preferencias que el usuario podrá modificar. Por ejemplo, el usuario podrá indicar con que frecuencia la aplicación ha de sincronizarse con el servidor o si queremos que se lancen notificaciones. Las preferencias también pueden utilizarse para que tu aplicación almacene información de forma permanente. En el capítulo 9 se estudiará cómo realizar esta función.
 

video[TutorialAñadir preferencias en Android

NOTA: A continuación se proponen una serie de ejercicios para añadir preferencias en Mis Lugares.  Para hacerlo en Asteroides puedes cambiarlos textos que aparecen, para que correspondan con la siguiente captura:

Ejercicio paso a paso: Añadiendo preferencias en la aplicación.


1.    Abre el proyecto Mis Lugares (o Asteroides)..

2.    Pulsa con el botón derecho sobre res y selecciona la opción New > Android resource file.

3.   Completa los campos File name: preferencias y Resource type: XML. Se creará el fichero res/xml/preferencias.xml.

4.    Edita este fichero. Selecciona la lengüeta Code  e introduce el siguiente código:

<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen
    xmlns:android="http://schemas.android.com/apk/res/android">
    <SwitchPreferenceCompat
        android:key="notificaciones"
        android:title="Mandar notificaciones"
        android:summary="Notificar si estamos cerca de un lugar"/>
    <EditTextPreference
        android:key="maximo"
        android:title="Máximo de lugares a mostrar"
        android:summary="Limita en número de valores que se muestran"
        android:inputType="number"
        android:defaultValue="12"/>
    <ListPreference
        android:key="orden"
        android:title="Criterio de ordenación"
        android:summary="Que lugares quieres que aparezcan antes"
        android:entries="@array/tiposOrden"
        android:entryValues="@array/tiposOrdenValores"
        android:defaultValue="0"/>
</androidx.preference.PreferenceScreen>

El significado de cada etiqueta y atributo se descubre fácilmente si observas el resultado obtenido que se muestra a continuación. El atributo inputType permite configurar el tipo de teclado que se mostrará para introducir el valor. Coinciden con el atributo de EditText. Para ver los posibles valores consultar: developer.android.com/reference/android/widget/TextView.html#attr_android:inputType
 

5.   Para almacenar los valores del desplegable, has de crear el fichero /res/values/arrays.xml con el siguiente contenido:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="tiposOrden">
        <item>vectorial</item>
        <item>bitmap</item>
        <item>3D</item>
    </string-array>
    <string-array name="tiposOrdenValores">
        <item>0</item>
        <item>1</item>
        <item>2</item>
    </string-array>
</resources> 

6.  Añade al fichero Gradle Scripts/Bulid.gradle(Module:app) la dependencia:


dependencies {
    …
    implementation 'androidx.preference:preference:1.1.1'
} 
7.     Crea una nueva clase PreferenciasFragment con el siguiente código:

public class PreferenciasFragment extends PreferenceFragmentCompat {
    @Override
    public void onCreatePreferences(Bundle savedInstanceState,
                                    String rootKey) {
        setPreferencesFromResource(R.xml.preferencias, rootKey);
    }
}
class PreferenciasFragment : PreferenceFragmentCompat() {
   override fun onCreatePreferences(savedInstanceState: Bundle?,
                                    rootKey: String?) {
      setPreferencesFromResource(R.xml.preferencias, rootKey)
   }
} 

Nota sobre Java/Kotlin: Pulsa Alt-Intro para que automáticamente se añadan los paquetes que faltan.

La clase PreferenceFragmentCompat permite crear un fragment que contiene una ventana con las opciones de preferencias definidas en un recurso XML. Un fragment es un elemento que puede ser incrustado dentro de una actividad. El uso de fragment se estudia con más detalle en la última unidad del curso.

8.     Ahora vamos a crear una actividad que simplemente muestre el fragment anterior. Crea la la clase PreferenciasActivity con el siguiente código:

public class PreferenciasActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getSupportFragmentManager().beginTransaction()
                .replace(android.R.id.content, new PreferenciasFragment())
                .commit();
    }
} 
class PreferenciasActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      supportFragmentManager.beginTransaction()
         .replace(android.R.id.content, PreferenciasFragment())
         .commit()
   }
} 

Desde una actividad podemos visualizar un fragment en tiempo de ejecución. Para ello utilizamos el manejador de fragments de la actividad (getSupportFragmentManager()) y comenzamos una transacción (beginTransaction()). Una transacción es una operación de insertado, borrado o reemplazo de fragments. En el ejemplo vamos a reemplazar el contenido de la actividad por un nuevo fragment de la clase PreferenciasFragment. Finalmente se llama a commit() para que se ejecute la transacción.

9.     No hay que olvidar registrar toda nueva actividad en AndroidManifest.xml.

10.     Añade a MainActivity.java el método lanzarPreferencias(). Este método ha de tener el mismo código que lanzarAcercaDe() pero lanzando la actividad PreferenciasActivity. En el layout activity_main.xml añade al botón con el texto «Configurar» en el atributo onClick el valor lanzarPreferencias.

11.   Para activar la configuración desde la opción de menú añade el siguiente código en el fichero MainActivity.java en el método onOptionsItemSelected().

if  (id == R.id.action_settings) {
   lanzarPreferencias(null);
   return true;
} 
R.id.action_settings -> {
   lanzarPreferencias()
   true
} 

Si has hecho la práctica “Casos de Uso para arrancar actividades” utiliza mejor el código usoActividades.lanzarPreferencias() para lanzar la actividad.

12.     Arranca la aplicación y verifica que puedes lanzar las preferencias mediante las dos alternativas.

NOTA: Si introduces un valor en el campo donde se ha indicado inputType="number", comprobarás que no funciona correctamente. Se trata de un bug de Google, todavía sin resolver. De momento nos recomiendan que usemos la siguiente librería https://github.com/Gericop/Android-Support-Preference-V7-Fix. Sigue los dos sencillos pasos indicados en README.md. 
 

Organizando preferencias

Cuando el número de preferencias es grande, resulta interesante organizarlas de forma adecuada. Una posibilidad consiste en dividirlas en varias pantallas, de forma que cuando se seleccione una opción en la primera pantalla, se abra una nueva pantalla de preferencias. Para organizar las preferencias de esta forma, usa el siguiente esquema:

<PreferenceScreen>
  <CheckBoxPreference …/>
  <EditTextPreference …/>
  …
  <PreferenceScreen android:title=”Modo multijugador”>
    <CheckBoxPreference …/>
    <EditTextPreference …/>
    …
  </PreferenceScreen>
</PreferenceScreen> 

Práctica: Organizando preferencias(I)

1. Crea una nueva lista de preferencias <PreferenceScreen> dentro de la lista de preferencias del fichero res/xml/preferencias.xml.

2. Asígnale al parámetro android:title el valor “Notificaciones por correo”.

3. Crea tres elementos dentro dentro de esta lista: Activar multijugador, Máximo de jugadores y Tipo de conexión. Para este último, han de poder escogerse los valores: Bluetooth, Wi-Fi e Internet.

Otra alternativa para organizar las preferencias consiste en agruparlas por categorías. Con esta opción se visualizarán en la misma pantalla, pero separadas por grupos. Has de seguir el siguiente esquema:

<PreferenceScreen>
  <CheckBoxPreference …/>
  <EditTextPreference …/>
  …
  <PreferenceCategory android:title=”Modo multijugador”>
    <CheckBoxPreference …/>
    <EditTextPreference …/>
.  . …
  </PreferenceCategory>
</PreferenceScreen> 

A continuación se representa la forma en la que Android muestra las categorías:

Práctica: Organizando preferencias(II)

Modifica la práctica anterior para que en lugar de mostrar las propiedades en dos pantallas, aparezcan en una sola, tal y como se muestra en la imagen anterior.

Como se almacenan las preferencias de usuario

Si un usuario modifica el valor de una preferencia, este quedará almacenado de forma permanente en el dispositivo. Para conseguir esta persistencia, Android almacena las preferencias seleccionadas en un fichero XML dentro de la carpeta data/data/nombre.del.paquete/files/shared_prefs, donde nombre.del.paquete ha de ser reemplazado por el paquete de la aplicación. El nombre del fichero para almacenar las preferencias de usuario ha de ser siempre nombre.del.paquete_preferences.xml. Esto significa que solo puede haber unas preferencias de usuario por aplicación. Como se estudiará más adelante, puede haber otros ficheros de preferencias; pero, a diferencia de las preferencias de usuario, no pueden ser editadas directamente por el usuario, si no que hay que acceder a ellas por código.

Ejercicio paso a paso: Donde se almacenan las preferencias de usuario

Veamos donde se han almacenado las preferencias que acabamos de crear:

1.    Para navegar por el sistema de ficheros de un dispositivo, desde Android Studio, selecciona la lengüeta Device File Explorer en la esquina inferior derecha.

2.    Busca el siguiente fichero:  /data/data/<nombre del paquete>/shared_prefs/ <nombre del paquete>_preferences.xml

3. Haz doble clic sobre el fichero para visualizar su contenido:

 
<map>
   <boolean name="notificaciones" value="true" /> 
   <string name="maximo">12</string> 
   <string name="orden">0</string> 
</map>

Accediendo a los valores de las preferencias

Por supuesto, será necesario acceder a los valores de las preferencias para alterar el funcionamiento de nuestra aplicación. El siguiente ejemplo nos muestra cómo realizarlo:

public void mostrarPreferencias(){
   SharedPreferences pref = 
            PreferenceManager.getDefaultSharedPreferences(this);
   String s = "música: " + pref.getBoolean("musica",true)
         +", gráficos: " + pref.getString("graficos","?");
   Toast.makeText(this, s, Toast.LENGTH_SHORT).show();
}

El código comienza creando el objeto pref de la clase SharedPreferences y le asigna las preferencias definidas para la aplicación. A continuación crea el String s y le asigna los valores de dos de las  preferencias. Se utilizan los métodos pref.getBoolean() y pref.getString(), que disponen de dos parámetros: el valor de key que queremos buscar ("musica" y "graficos") y el valor asignado por defecto en caso de no encontrar esta key.

Finalmente se visualiza el resultado utilizando la clase Toast. Los tres parámetros indicados son el contexto (nuestra actividad),  el String a mostrar y el tiempo que se estará mostrando esta información.

Ejercicio paso a paso: Accediendo a los valores de las preferencias

1. Copia la función anterior en la actividad principal. Añade el parámetro que se muestra a continuación  mostrarPreferencias(View view).

2. Asígna el atributo onClick del botón Salir o Jugar al mé anterior, según estés en Mislugares o Asteroides.

3. Visualiza también el resto de las preferencias que hayas introducido.

Verificar valores correctos


En muchas ocasiones vas a querer limitar los valores que un usuario puede introducir en las preferencias. Por ejemplo, podría ser interesante que el valor introducido por el usuario en la preferencia número de fragmentos solo pudiera tomar valores entre 0 y 9. Para conseguir esto podemos utilizar el escuchador de evento onPreferenceChangeListener que podremos asignar a una preferencia. Veamos cómo actuar en el siguiente ejercicio:

Ejercicio paso a paso: Verificar valores correctos de una preferencia.

 

1.   Copia este código al final del método onCreate() en PreferenciasFragment:

 
final EditTextPreference fragmentos = (EditTextPreference) 
                                             findPreference("maximo");
fragmentos.setOnPreferenceChangeListener(
  new Preference.OnPreferenceChangeListener() {
    @Override
    public boolean onPreferenceChange(Preference preference, Object 
                                                              newValue) {
       int valor;
       try {
          valor = Integer.parseInt((String)newValue);
       } catch(Exception e) {
          Toast.makeText(getActivity(), "Ha de ser un número", 
                                              Toast.LENGTH_SHORT).show();
          return false;
       }
       if (valor>=0 && valor<=99) {
          fragmentos.setSummary(
               "Limita en número de valores que se muestran ("+valor+")");
          return true;
       } else {
          Toast.makeText(getActivity(), "Valor Máximo 99", 
                                              Toast.LENGTH_SHORT).show();
          return false;
       }
    }
  });
 

NOTA: Si trabajas con Asteroides reemplaza "maximo" por "fragmentos" y cambia los textos necesarios para que correspondan con esta propiedad.

El código comienza obteniendo una referencia de la preferencia para asignarle un escuchador que será llamado cuando cambie su valor. El escuchador comienza convirtiendo el valor introducido a entero. Si se produce un error es porque el usuario no ha introducido un valor adecuado. En este caso, mostramos un mensaje y devolvemos false para que el valor de la preferencia no sea modificado. Si no hay error, tras verificar el rango de valores aceptables, modificamos la explicación de la preferencia para que aparezca el nuevo valor entre paréntesis y devolvemos true para aceptar este valor. Si no está en el rango, mostramos un mensaje indicando el problema y devolvemos false.

2.   Ejecuta el proyecto y verifica que funciona correctamente.

Práctica: Mostrar el valor de una preferencia

En el ejercicio anterior cuando se modifica el número de fragmentos se muestra entre paréntesis el nuevo valor introducido. El funcionamiento no es del todo correcto, cuando entramos por primera vez o cuando se cambia el teléfono de vertical a horizontal este valor no se muestra. Añade el código necesario para que este valor aparezca siempre.

Preguntas de repaso y reflexión: preferencias de usuario

Introducción unidad 5

Comenzamos la unidad introduciendo el uso de RecyclerView,  un elemento de gran importancia en el diseño de una interfaz de usuario en Android. Nos va a permitir definir una lista de elementos personalizados.

En esta unidad también trataremos  las intenciones. Son una herramienta fundamental en Android, que nos permite lanzar componentes para realizar tareas en nuestra aplicación. Estos componentes podrán ser parte de nuestra aplicación, por ejemplo como hicimos en la unidad anterior, para arrancar nuevas actividades. También nos van a permitir lanzar componentes de otras aplicaciones, o del sistema, para realizar tareas como tomar una fotografía o realizar una llamada de teléfono.

Terminaremos la unidad añadiendo a la aplicación Mis Lugares la capacidad de trabajar con fotografías y con fechas.

Objetivos:

  • Estudiar una vista para crear listas en Android: RecyclerView.
  • Describir el uso de intenciones para invocar actividades estándar en Android.
  • Enumerar las intenciones más útiles para realizar acciones estándar en Android.
  • Aprender a manipular fichero con fotografías y mostrarlos en un ImageView.
  • Mostrar el uso de ventanas de dialogo para seleccionar fechas.

Programa Mooc

   www.androidcurso.com

Programa completo del curso MOOC de la UPV:

“Android: Introducción a la Programación” con Kotlin

Precourse

La aplicación Mis Lugares http://www.androidcurso.com/index.php/569
Qué lenguaje escoger: Java o Kotlin http://www.androidcurso.com/index.php/927
Instalación del entorno de desarrollo http://www.androidcurso.com/index.php/23
 

Unidad 1  Repaso de Java y Koltin

JAVA

Sobre el curso de Java http://www.androidcurso.com/index.php/21 
Características de Java http://www.androidcurso.com/index.php/22
Creación de clases en Java        http://www.androidcurso.com/index.php/24
Creación y utilización de objetos          http://www.androidcurso.com/index.php/25
Comentarios y documentación javadoc http://www.androidcurso.com/index.php/27
El encapsulamiento y la visibilidad en Java http://www.androidcurso.com/index.php/32
La clase Lugar http://www.androidcurso.com/index.php/460
La herencia en Java      http://www.androidcurso.com/index.php/29
La sobrecarga en Java   http://www.androidcurso.com/index.php/30
El polimorfismo en Java http://www.androidcurso.com/index.php/31
Clases abstractas, interfaces y herencia múltiple http://www.androidcurso.com/index.php/686
Enumerados: La clase TipoLugar http://www.androidcurso.com/index.php/461
Las colecciones en Java http://www.androidcurso.com/index.php/462
La clase Lugares http://www.androidcurso.com/index.php/929 
Referencia Java http://www.dcomg.upv.es/~jtomas/android/ficheros/Referencia_Java.pdf

KOTLIN
Sobre el curso de Kotlin http://www.androidcurso.com/index.php/928    
Introducción a Kotlin  http://www.androidcurso.com/index.php/910 
https://youtu.be/3Gbs3k4qDIk
Estructuras de control en Kotlin http://www.androidcurso.com/index.php/912
https://youtu.be/xdK6p02qt-I
Variables en Kotlin http://www.androidcurso.com/index.php/911
https://youtu.be/NiPalSFEnnU
Funciones en Kotlin http://www.androidcurso.com/index.php/913
https://youtu.be/5HktdjV0sEc
Lambdas y Variables función en Kotlin http://www.androidcurso.com/index.php/914
https://youtu.be/YuiXeTHX3b0
Funciones inline en Kotlin  http://www.androidcurso.com/index.php/915
https://youtu.be/JIsCPFKULLk
Clases en Kotlin  http://www.androidcurso.com/index.php/916
https://youtu.be/e2qzU01L0II
Clases de datos en Kotlin  http://www.androidcurso.com/index.php/917
https://youtu.be/0mOLou_J808
La clase Lugar   http://www.androidcurso.com/index.php/460
Tipos de clases en Kotlin: abiertas, abstractas, internas y con alias  http://www.androidcurso.com/index.php/918
https://youtu.be/sMbrh3-1ufA
Clases abstractas, interfaces y herencia múltiple http://www.androidcurso.com/index.php/686
Clases selladas en Kotlin http://www.androidcurso.com/index.php/919
https://youtu.be/Yv8wafvZ9J8
Clases enumeradas en Kotlin http://www.androidcurso.com/index.php/920
https://youtu.be/aevBzJf-GOE
Enumerados: La clase TipoLugar http://www.androidcurso.com/index.php/461
Anko: Androd + Kotlin http://www.androidcurso.com/index.php/921
https://youtu.be/hRmOEkWROaA

Tratamiento de null en Kotlin http://www.androidcurso.com/index.php/922
https://youtu.be/h3w3J3kRX9A

El operador let en Kotlin http://www.androidcurso.com/index.php/923
https://youtu.be/mGqglXcUH-E

Inicialización de propiedades: lateinit vs lazy  http://www.androidcurso.com/index.php/926
https://youtu.be/6CwX8XjuDs0

Colecciones en Kotlin: introducción http://www.androidcurso.com/index.php/924
https://youtu.be/ef7WksHo1WU

Colecciones en Kotlin: List, Set y Map http://www.androidcurso.com/index.php/925
La clase Lugares http://www.androidcurso.com/index.php/929
https://youtu.be/WenqxF-RIyI


 

Unidad 2 Introducción a Android y entorno de desarrollo

Introducción a la unidad  http://www.androidcurso.com/index.php/96

¿Que hace a Android especial? http://www.androidcurso.com/index.php/97

Las plataformas para móviles http://www.androidcurso.com/index.php/98

Arquitectura de Android http://www.androidcurso.com/index.php/99

Descripción de las versiones de Android http://www.androidcurso.com/index.php/146

Elegir la versión en una aplicación Android http://www.androidcurso.com/index.php/463

Las librerías de compatibilidad (support library) http://www.androidcurso.com/index.php/562

Creación de dispositivos virtuales (AVD) http://www.androidcurso.com/index.php/465

Un primer programa en Android http://www.androidcurso.com/index.php/147

Ejecución del programa http://www.androidcurso.com/index.php/221

Ficheros y directorios de un proyecto Android http://www.androidcurso.com/index.php/148

Componentes de una aplicación http://www.androidcurso.com/index.php/149

Documentación y ApiDemos http://www.androidcurso.com/index.php/260

Depurar con el entorno de desarrollo http://www.androidcurso.com/index.php/564

Depurar con mensajes Log http://www.androidcurso.com/index.php/470

Unidad 3 Interfaz de usuario

Introducción a la unidad  http://www.androidcurso.com/index.php/110

Creación de una interfaz de usuario por código  http://www.androidcurso.com/index.php/111

Creación de una interfaz de usuario usando XML  http://www.androidcurso.com/index.php/112

Diseño visual de vistas  http://www.androidcurso.com/index.php/113

Los Layouts en Android  http://www.androidcurso.com/index.php/114

Uso de constraitlayout http://www.androidcurso.com/index.php/881

Introducción a Material Design  http://www.androidcurso.com/index.php/688

Definición de la paleta de colores de la aplicación  http://www.androidcurso.com/index.php/689

Mis Lugares: Primeras actividades http://www.androidcurso.com/index.php/115

Uso de recursos alternativos en Android http://www.androidcurso.com/index.php/116

Tipos de recursos       http://www.androidcurso.com/index.php/222

Recursos del sistema  http://www.androidcurso.com/index.php/471

Estilos y Temas en Android   http://www.androidcurso.com/index.php/118

Uso práctico de Vistas           http://www.androidcurso.com/index.php/119

View Binding          http://www.androidcurso.com/index.php/963

Referencia clase View            http://www.androidcurso.com/index.php/referencias/referencia-clase-view

Referencia recursos alternativos        http://www.dcomg.upv.es/~jtomas/android/ficheros/recursos_alternativos.pdf

Unidad 4    Actividades

Introducción a la unidad         http://www.androidcurso.com/index.php/475

Actividades en Android         http://www.androidcurso.com/index.php/122

Intercambio de datos entre actividades          http://www.androidcurso.com/index.php/123

La Barra de Accioneshttp://www.androidcurso.com/index.php/473

Creación y uso de iconos       http://www.androidcurso.com/index.php/125

Añadir preferencias en Android        http://www.androidcurso.com/index.php/476

Acceder a objetos globales de la aplicación   http://www.androidcurso.com/index.php/931

Uso de la arquitectura Clean  http://www.androidcurso.com/index.php/932

Creando actividades en Mis Lugareshttp://www.androidcurso.com/index.php/474

Unidad 5    RecyclerView e Intenciones

Introducción a la unidad         http://www.androidcurso.com/index.php/482

La vista RecyclerView  http://www.androidcurso.com/index.php/691

Las intenciones en Android   http://www.androidcurso.com/index.php/130

Uso de intenciones en Mis Lugares   http://www.androidcurso.com/index.php/483

Añadiendo fotografías en Mis Lugares http://www.androidcurso.com/index.php/484

Unidad 6    Ciclo de vida, multimedia y seguridad

Introducción a la unidad         http://www.androidcurso.com/index.php/488
Ciclo de vida de una actividad           http://www.androidcurso.com/index.php/158
¿Qué proceso se elimina?       http://www.androidcurso.com/index.php/277
Guardar el estado de las Actividades en Android     http://www.androidcurso.com/index.php/163
Multimedia en Android          http://www.androidcurso.com/index.php/164
Los tres pilares de la seguridad en Android  http://www.androidcurso.com/index.php/280
La firma digital https://youtu.be/Za19Smqc204
Usuario Linux y acceso a ficheros     http://www.androidcurso.com/index.php/281
Los permisos en Android      http://www.androidcurso.com/index.php/282
Permisos en Android 6 Marshmallow http://www.androidcurso.com/index.php/626

Unidad 7    Posicionamiento y mapas

Introducción a la unidad         http://www.androidcurso.com/index.php/490
Sistemas de geolocalización en dispositivos móviles http://www.androidcurso.com/index.php/491
Los Sistemas de posicionamiento global por Satélite (GPS) https://youtu.be/AnTMd3MFr4c
El API de Localización de Android   http://www.androidcurso.com/index.php/284
Emulación del GPS    http://www.androidcurso.com/index.php/285
Estrategias de Localización en Android         http://www.androidcurso.com/index.php/492
Google Maps http://www.androidcurso.com/index.php/223
Añadiendo mapas en Mis Lugares    http://www.androidcurso.com/index.php/493

Unidad 8    Bases de datos y Fragments en Android

Introducción a la unidad         http://www.androidcurso.com/index.php/494
Almacenamiento Datos en Android   http://www.androidcurso.com/index.php/297
Uso de base de datos en Android      http://www.androidcurso.com/index.php/495
Operaciones con bases de datos en Mis Lugares   http://www.androidcurso.com/index.php/497
Los Fragments  http://www.androidcurso.com/index.php/499
Diálogos de selección de fecha y hora http://www.androidcurso.com/index.php/485
Uso de Fragments en Mis Lugares  http://www.androidcurso.com/index.php/498

Infórm

ate Inf