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