Introducción a los Fragments en Android

Hace unos meses os escribía algunos consejos de buenas prácticas con Fragments en el artículo «Buenas Prácticas usando Fragments en Android».

Hoy vuelvo para contaros en profundidad sobre algunas de las cosas que hablé en su día, pero desde un punto de vista más de principiante. Vamos a tratar pues, no lo que es un Fragment, cosa que podéis consultar en el artículo anterior pero sí algunas de las operaciones más comunes y como funcionan.

Cargando un Fragment:
Un Fragment siempre se carga dentro de un Activity, esto es, que no puede existir sin ella. Generalmente usamos al Fragment para sustituir al Activity en el manejo de la interfaz, y dejamos que esta última se encarga de lógicas más complicadas e intercambiar Fragments. Sin embargo, también es posible cargar un Fragment dentro de un trozo del layout del Activity, y hacer que los dos manejen interfaz. Este último caso es desaconsejable y debe dejarse sólo para interfaces complejas.

A continuación, vamos a ver dos modos de cargar un Fragment

– Cargar un Fragment de manera estática
Los Fragments pueden incluirse en el fichero xml del layout, y luego cargarlos desde el Activity usando el setContentView() como haríamos habitualmente.

Si tenemos el siguiente Activity, cuyo layout es activity_main.xml :

		public abstract class MainActivity extends Activity{
		
			@Override
			public void onCreate(Bundle saveInstanceState){
				super.onCreate(saveInstanceState);
				setContentView(R.layout.activity_main);
			}
		}
	

Y configuramos el layout de la siguiente manera:

	<?xml version="1.0" encoding="utf-8"?>
	<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
		xmlns:tools="http://schemas.android.com/tools"
		android:orientation="vertical"
		android:layout_width="match_parent"
		android:layout_height="match_parent">

		<fragment
			android:layout_width="wrap_content"
			android:layout_height="match_parent"
			android:id= "@+id/content"
			class="com.gpmess.app.MainFragment"
			tools:layout="@layout/fragment_reset">

		</fragment>

	</LinearLayout>
	

Lo que estamos haciendo es cargar el Fragment que se encuentra en la propiedad «class» del tag en este layout, en este caso MainFragment.

Luego, MainFragment, cuyo layout es fragment_main.xml sería algo así:

		public class MainFragment extends Fragment{
		
			public static final String TAG = "MainFragment";
		
			public static MainFragment newInstance(Bundle params){
				MainFragment mf = new MainFragment();
				mf.setArguments(params);
				return mf;
			}
			
			@Override
			public void onCreateView(Bundle saveInstanceState, LayoutInflater inflater, ViewGroup container){
				super.onCreateView(saveInstanceState, inflater, container);
				return inflater.inflate(R.layout.fragment_main, container, false);
			}
		
		}
	

De este modo, lo que finalmente veríamos al cargar MainActivity es, en realidad MainFragment con su layout fragment_main.xml.

Este método es más sencillo que el segundo, pero tiene un inconveniente. El Fragment cargado de este modo se incluye de manera estática y no puede ser sustituido por otro Fragment posteriormente usando una FragmentTransaction. Además, el tag del layout deberá tener OBLIGATORIAMENTE la propiedad android:id para que pueda ser cargado. Por otro lado, es muy útil si no tenéis previsión de andar cambiando de Fragment dentro de ese Activity en concreto.

– Cargando un Fragment de manera dinámica
Esta es la manera que veíamos en el anterior artículo. Supongamos que tenemos el mismo activity que antes, MainActivity con su layout activity_main.xml y el mismo MainFragment con su layout fragment_main.xml.

Si cargamos el fragment de esta manera, el Activity no necesitaría el layout y podríamos obviar el setContentView()

		public abstract class MainActivity extends Activity{
		
			@Override
			public void onCreate(Bundle saveInstanceState){
				super.onCreate(saveInstanceState);
				FragmentTransaction ft = getFragmentManager().beginTransaction();
				ft.replace(android.R.id.content, MainFragment.newInstance(null), MainFragment.TAG);
				ft.commit();
			}
		}	
	

Observemos que estamos haciendo aquí. Estamos creando una nueva FragmentTransaction, que representa una operación para añadir, quitar o sustituir un Fragment.

Para ello usamos el FragmentManger, que se recupera utilizando el método getFragmentManager() (o getSupportFragmentManager() si estáis dando compatibilidad a Android 2.3 y anteriores). En esta FragmentTransaction hacemos un replace. A este replace debemos decirle que elemento es el que queremos sustituir por el segundo parámetro, que es el MainFragment. En este caso, le pasamos «android.R.id.content», y esto tiene una explicación.

Cuando no se especifica ningún layout a un Activity, este tiene un layout por defecto. Dentro de este, existe un FrameLayout cuyo id es «android.R.id.content». Por tanto, lo que estamos haciendo es sustituir el layout «vacío» del Activity por MainFragment, y este cargará el layout fragment_main a través de su método onCreateView().

– Cambiando entre Fragments a través del Activity y pasando mensajes Fragments
Supongamos que tenemos el mismo MainFragment de antes, pero además ahora le vamos a añadir comunicación con el Activity. La clase quedaría de este modo:

		public class MainFragment extends Fragment{
		
			public static final String TAG = "MainFragment";
			private OnFragmentIterationListener listener;
			private Button button;
			
			
			@Override
			public void onAttach(Activity activity){
				try{
					listener = (OnFragmentIterationListener) activity;
				}catch(ClassCastException ex){
					Log.e(TAG, "El Activity debe implementar la interfaz onFragmentIterationListener";
				}
			}
		
		
			public static MainFragment newInstance(Bundle params){
				MainFragment mf = new MainFragment();
				mf.setArguments(params);
				return mf;
			}
			
			@Override
			public void onCreateView(Bundle saveInstanceState, LayoutInflater inflater, ViewGroup container){
				super.onCreateView(saveInstanceState, inflater, container);
				View v =  inflater.inflate(R.layout.fragment_main, container, false);
				button = (Button) v.findViewById(R.id.button);
				button.setOnClickListener(new View.OnClickListener(){
				
					@Override
					public void onClick(View v){
						if(listener != null){
							listener.onFragmentIteration("Cambio de Fragment");
						}
					}
				});
			}
		
		}
		
		public interface OnFragmentIterationListener{
			public void onFragmentIteration(Object object);
		
		}
	

Como os contaba en el otro artículo, cuando utilizamos una interfaz, el Activity debe implementarla, en este caso OnFragmentIterationListener. De este modo, cuando hacemos listener.onFragmentIteration() lo que estamos haciendo es llamar al método onFragmentIteration() del Activity. Dentro de este método, es dónde creamos el nuevo Fragment y sustituimos al actual. Por tanto el Activity quedaría así:

		public abstract class MainActivity extends Activity implements MainFragment.OnFragmentIterationListener{
		
			@Override
			public void onCreate(Bundle saveInstanceState){
				super.onCreate(saveInstanceState);
				FragmentTransaction ft = getFragmentManager().beginTransaction();
				ft.replace(android.R.id.content, MainFragment.newInstance(null), MainFragment.TAG);
				ft.commit();
			}
			
			@Override
			public void onFragmentIteration(Object object){
				String message = (String) object;
				Bundle bundle = new Bundle();
				bundle.putString("message", message);
				FragmentTransaction ft = getFragmentManager().beginTransaction();
				ft.replace(android.R.id.content, SecondFragment.newInstance(message), SecondFragment.TAG);
				ft.addToBackStack(null);
				ft.commit();
			}
		}	
	

Cómo os comentaba, cuando en MainFragment hacemos listener.onFragmentIteration() se ejecuta este método en el Activity. Ahora, en este método en el Activity, lo que hacemos es crear un SecondFragment (cuya definición sería similar a la de MainFragment) y hacer de nuevo una FragmentTransaction. Con esto, hacemos de nuevo un replace de manera similar a como hacíamos en el onCreate, con lo que cambiamos el Fragment que estaba Activity (MainFragment) por otro nuevo Fragment, que en este caso es SecondFragment. Además, hemos añadido un ft.addToBackStack(). Con esto último lo que conseguimos es poder revertir esta operación, es decir, que cuando le demos al botón de atrás podamos volver a MainFragment, ya que si no lo hiciésemos diréctamente saldríamos del Activity. Si os fijais, no hemos hecho esto en la FragmentTransaction del método onCreate, ya que no queremos que cuando pulsemos atrás se nos quede el Activity en blanco, que es lo que pasaría.

Usando el patrón newInstance del que os hablé en el otro artículo, SecondFragment recibirá el mensaje «Cambio de Fragment» que le pasamos desde MainFragment.