Implementar Maestro-Detalle con Fragments en Android

Buenas tardes, debido a que he recibido varios comentarios o peticiones sobre crear aplicaciones con Fragment y la funcionalidad de Maestro-Detalle, me he decidido a subir esta entrada.


La funcionalidad Maestro-Detalle es importante cuando vamos a desarrollar aplicaciones para dispositivos que son Tablet, ya que gracias a los Fragment podemos tener varios paneles a la vez en la pantalla. En el caso de que nuestra apiicación tenga como uso final solo los SmartPhone, esta funcionalidad no es mala idea, pero no le sacaríamos todo el potencial, ya que la diferencia entre tamaños de pantalla es bastante grande (aunque cada vez menos). En el caso de que nuestra aplicación tenga como finalidad cubrir los SmartPhone y las Tablets, es buena idea implementar este tipo de funcionalidad, ya que podemos controlar cuando el dispositivo es Tablet o SmartPhone, y así con una sola aplicación cubrir ambos dispositivos. Ahora mismo parece una locura, pero lo veremos a lo largo de la entrada.

Con esta pequeña introducción nos ponemos manos a la obra:

  • Creación del Proyecto

    Para comenzar vamos a crear un proyecto de aplicación Android. Para ello nos dirigiremos a File->New->Android Application Project. Si no encontramos esta opción vamos a Other, y en la lista que nos aparece buscamos la opción deseada. También podemos introducir “Android” en el campo de texto para filtrar los resultados y que sea mas rápido.

    Una vez aceptemos el tipo de proyecto, nos aparecerá el asistente de Android, en el cual a través de unos simples pasos tendremos nuestro proyecto creado. En la primera ventana se nos da a elegir el nombre de nuestra aplicación, al igual que el nombre del proyecto. En mi caso lo he llamado “EjemploMaestroDetalle” aunque el nombre es lo de menos y podéis asignarle el que querais. El resto podeis dejarlo como querais o cambiar el rango de versiones de Android que vais a cubrir según vuestras necesidades.

    A medida que avancemos nos aparecerá la opción de crear nuestra primera Activity y su layout, en mi caso he dejado el nombre como está, MainActivity y su layout como activity_main.

    Una vez terminemos el asistente, tendremos nuestro proyecto listo y creado para seguir construyendo nuestra aplicación.

  • TituloFragment

    Antes de meternos con la Activity vamos a crear lo necesario para montar todo en la Activity. Para esta funcionalidad vamos a necesitar 2 Fragment, uno será un ListFragment que contendrá una lista de lo que sea, y otro Fragment que contendrá el contenido adecuado del elemento pulsado en el ListFragment.

    En este punto vamos a desarrollar la funcionalidad e importancia que tendrá este ListFragment. Vamos a ver su código, que luego explicaremos paso a paso:

    		public class TituloFragment extends ListFragment {
    
    			onTituloSelectedListener mCallback;
    		
    			// Interface que la Activity contenedora debe implementar
    			// para poder tener comunicación
    			public interface onTituloSelectedListener {
    				public void onTituloSelected(int position);
    			}
    		
    			@Override
    			public void onCreate(Bundle savedInstanceState) {
    				// TODO Auto-generated method stub
    				super.onCreate(savedInstanceState);
    				
    				// Establecemos el Adapter cuando se crea el Fragment
    				setListAdapter(new ArrayAdapter<String>(getActivity(),
    						android.R.layout.simple_list_item_1, Contenido.titulos));
    			}
    		
    			@Override
    			public void onAttach(Activity activity) {
    				// TODO Auto-generated method stub
    				super.onAttach(activity);
    				// Inicializamos nuestra variable de referencia del tipo
    				// onTituloSelectedListener junto con el valor del objeto
    				// activity que debe ser una Activity que implemente esta interface
    				try {
    					mCallback = (onTituloSelectedListener) activity;
    				} catch (ClassCastException e) {
    					Log.d("ClassCastException",
    							"La Activity debe implementar esta Interface");
    				}
    			}
    		
    			@Override
    			public void onListItemClick(ListView l, View v, int position, long id) {
    				// TODO Auto-generated method stub
    				super.onListItemClick(l, v, position, id);
    				
    				// Llamamos al método que implementa la Activity pasandole
    				// la posicion del elemento que hemos pulsado
    				mCallback.onTituloSelected(position);
    			}
    		
    		}
    		

    Como podemos observar en grandes rasgos, vemos que tiene una interface, carga un array en la lista y poco más. Vamos a analizar los elementos:

    • onTituloSelectedListener

      Esta interface es la que vamos a usar para comunicar este ListFragment con el otro Fragment, y esto se hará a través de la FragmentActivity o Activity.

      				public interface onTituloSelectedListener {
      					public void onTituloSelected(int position);
      				}
      				

    • onCreate

      En este método, perteneciente al ciclo de vida del Fragment vamos a establecer el ArrayAdapter en la lista, para cargar una serie de elementos.

      				@Override
      				public void onCreate(Bundle savedInstanceState) {
      					// TODO Auto-generated method stub
      					super.onCreate(savedInstanceState);
      					
      					// Establecemos el Adapter cuando se crea el Fragment
      					setListAdapter(new ArrayAdapter<String>(getActivity(),
      							android.R.layout.simple_list_item_1, Contenido.titulos));
      				}
      				

    • onAttach

      En este método, también perteneciente al ciclo de vida de los Fragment vamos a poder instanciar la variable de instancia que hemos definido, que es del tipo onTituloSelectedListener, exactamente, la interface que hemos creado. Esto lo hacemos para que pueda haber comunicación entre nuestra Activity y este Fragment. En el caso de que la Activity no implemente la interface recibiremos una excepción del tipo ClassCastException.

      				@Override
      				public void onAttach(Activity activity) {
      					// TODO Auto-generated method stub
      					super.onAttach(activity);
      					// Inicializamos nuestra variable de referencia del tipo
      					// onTituloSelectedListener junto con el valor del objeto
      					// activity que debe ser una Activity que implemente esta interface
      					try {
      						mCallback = (onTituloSelectedListener) activity;
      					} catch (ClassCastException e) {
      						Log.d("ClassCastException",
      								"La Activity debe implementar esta Interface");
      					}
      				}
      				

    • onListItemClick

      Este método viene con la clase ListFragment y nos debe de ser ya conocido de los ListView. Lo único que hará es llamar al método que la Activity implementa, pasandole la posición del elemento que hemos pulsado, y en la Activity se redirige el resto.

      				@Override
      				public void onListItemClick(ListView l, View v, int position, long id) {
      					// TODO Auto-generated method stub
      					super.onListItemClick(l, v, position, id);
      					
      					// Llamamos al método que implementa la Activity pasandole
      					// la posicion del elemento que hemos pulsado
      					mCallback.onTituloSelected(position);
      				}
      				

  • ContenidoFragment

    Esta es la clase del otro Fragment que vamos a usar y será el encargado de mostrar el contenido para un elemento que pulsemos en el ListFragment. En este Fragment debemos implementar algún método más, ya que recibirá una posición y tendremos que tratar con ella para mostrar el contenido.

    Vamos a ver su código para luego comentar cada método que necesitemamos implementar:

    		public class ContenidoFragment extends Fragment {
    			public static final String POSICION = "position";
    			int position = -1;
    		
    			@Override
    			public View onCreateView(LayoutInflater inflater, ViewGroup container,
    					Bundle savedInstanceState) {
    				// TODO Auto-generated method stub
    		
    				// Comprobamos si se recupera de un estado anterior
    				if (savedInstanceState != null) {
    					position = savedInstanceState.getInt("position");
    				}
    		
    				return inflater.inflate(R.layout.contenido_fragment, container, false);
    			}
    		
    			@Override
    			public void onStart() {
    				// TODO Auto-generated method stub
    				super.onStart();
    		
    				// Comprobamos si tenemos argumentos
    				Bundle args = getArguments();
    				if (args != null) {
    					// Si tenemos argumentos, establecemos la posicion
    					actualizarContenido(args.getInt(POSICION));
    				} else if (position != -1) {
    					// Si la variable de instancia es diferente a -1
    					// quiere decir que nos hemos recuperado de un estado anterior
    					// y actualizamos el contenido
    					actualizarContenido(position);
    				}
    			}
    		
    			public void actualizarContenido(int position) {
    		
    				// Instanciamos el TextView y establecemos el contenido
    				TextView tvContenido = (TextView) getActivity().findViewById(
    						R.id.tvContenido);
    				tvContenido.setText(Contenido.descripcion[position]);
    		
    				// Guardamos la posicion del elemento que estamos consultando
    				this.position = position;
    			}
    		
    			@Override
    			public void onSaveInstanceState(Bundle outState) {
    				// TODO Auto-generated method stub
    				super.onSaveInstanceState(outState);
    		
    				// Guardamos el estado de la posicion del elemento
    				// que estábamos consultando
    				outState.putInt(POSICION, position);
    			}
    		
    		}
    		

    Analicemos todo por partes:

    • onCreateView

      Este método pertenece al ciclo de vida de los Fragment. en él vamos a crear el View del Fragment, que será “inflado” por el objeto de la clase LayoutInflater haciendo uso de un layout muy simple, el cuál únicamente contiene un TextView.

      Observamos también que tratamos el objeto savedInstanceState para comprobar si el Fragment se está recuperando de un estado anterior. Con esto controlaremos que si por algún caso hemos cambiado orientación de la pantalla o algo, al recrearse el Fragment volvamos a tener disponible el contenido que estábamos usando, y no ninguno en su lugar.

      				@Override
      				public View onCreateView(LayoutInflater inflater, ViewGroup container,
      						Bundle savedInstanceState) {
      					// TODO Auto-generated method stub
      			
      					// Comprobamos si se recupera de un estado anterior
      					if (savedInstanceState != null) {
      						position = savedInstanceState.getInt("position");
      					}
      			
      					return inflater.inflate(R.layout.contenido_fragment, container, false);
      				}
      				

    • actualizarContenido

      Este método debemos crearlo y será el encargado de recibir un entero con un valor, el cual trataremos. Instanciaremos el TextView del layout, para tener acceso a el y a continuación estableceremos su contenido en base al entero que hemos recibido.

      				public void actualizarContenido(int position) {
      
      					// Instanciamos el TextView y establecemos el contenido
      					TextView tvContenido = (TextView) getActivity().findViewById(
      							R.id.tvContenido);
      					tvContenido.setText(Contenido.descripcion[position]);
      			
      					// Guardamos la posicion del elemento que estamos consultando
      					this.position = position;
      				}
      				

    • onStart

      Este método también pertenece al ciclo de vida de los Fragment y lo usaremos para llamar al método actualizarContenido. Este método se ejecuta cuando el Fragment ya ha sido creado, y se ha empezado a ejecutar. Comprobamos si tiene algún argumento mediante el uso de la clase Bundle. Si este objeto no es null vamos a extraer el dato guardado y lo vamos a mandar al método actualizarContenido el cual tratará con el entero que hemos extraído. En el caso de que nuestra variable de instancia tenga un valor de -1, vamos a actualizar el contenido con el valor de nuestra variable de instancia, lo que quiere decir que el Fragment se ha recuperado de un estado anterior.

      				@Override
      				public void onStart() {
      					// TODO Auto-generated method stub
      					super.onStart();
      			
      					// Comprobamos si tenemos argumentos
      					Bundle args = getArguments();
      					if (args != null) {
      						// Si tenemos argumentos, establecemos la posicion
      						actualizarContenido(args.getInt(POSICION));
      					} else if (position != -1) {
      						// Si la variable de instancia es diferente a -1
      						// quiere decir que nos hemos recuperado de un estado anterior
      						// y actualizamos el contenido
      						actualizarContenido(position);
      					}
      				}
      				

    • onSaveInstanceState

      Este método lo vamos a usar para guardar el estado del Fragment, ya que así podremos restaurarlo y volver a la aplicación justo por donde lo dejamos.

      				@Override
      				public void onSaveInstanceState(Bundle outState) {
      					// TODO Auto-generated method stub
      					super.onSaveInstanceState(outState);
      			
      					// Guardamos el estado de la posicion del elemento
      					// que estábamos consultando
      					outState.putInt(POSICION, position);
      				}
      				

  • MainActivity

    Ahora sí vamos a implementar lo necesario para que nuestra aplicación funcione tanto en dispositivos SmartPhones como Tablets adaptando el diseño de la UI.

    Para comenzar vamos a ver que debemos crear 2 archivos xml para su layout. Uno vendrá ya creado y vamos a ver su contenido a continuación:

    		<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    		    xmlns:tools="http://schemas.android.com/tools"
    		    android:id="@+id/fragment_container"
    		    android:layout_width="match_parent"
    		    android:layout_height="match_parent"
    		    tools:context=".MainActivity" >
    		
    		</FrameLayout>
    		

    Como vemos tenemos un elemento FrameLayout el cuál será el contenedor del Fragment. Este layout será el que usará Android en caso de que el dispositivo sea SmartPhone.

    Ahora vamos a ir a la carpeta res/, donde crearemos otra carpeta dentro de ella, llamado layout-large. En esta carpeta vamos a crear un nuevo archivo layout, con el mismo nombre que el anterior, “activity_main.xml”. Este layuot contendrá lo siguiente:

    		<?xml version="1.0" encoding="utf-8"?>
    		<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    		    android:layout_width="match_parent"
    		    android:layout_height="match_parent"
    		    android:baselineAligned="false"
    		    android:orientation="horizontal" >
    		
    		    <fragment
    		        android:id="@+id/tituloFragment"
    		        android:name="sekth.droid.maestrodetalle.Fragments.TituloFragment"
    		        android:layout_width="0dp"
    		        android:layout_height="match_parent"
    		        android:layout_weight="1" />
    		
    		    <fragment
    		        android:id="@+id/contenidoFragment"
    		        android:name="sekth.droid.maestrodetalle.Fragments.ContenidoFragment"
    		        android:layout_width="0dp"
    		        android:layout_height="match_parent"
    		        android:layout_weight="3" />
    		
    		</LinearLayout>
    		

    vemos que al contrario que el anterior, vamos a tener un LinearLayout con orientación horizontal y almacenará 2 Fragment. Esto es así ya que en el caso de ejecutar la aplicación en una Tablet, directamente se usará este layout y tendremos nuestra funcionalidad Maestro-Detalle.

    Bien, ya tenemos los layouts, pero ahora tenemos que manejar como usarlos junto a nuestros Fragments y los distintos tipos de pantalla, vamos a ello.

    En el código de nuestra Activity tendremos lo siguiente:

    		public class MainActivity extends FragmentActivity implements
    				TituloFragment.onTituloSelectedListener {
    		
    			@Override
    			protected void onCreate(Bundle savedInstanceState) {
    				super.onCreate(savedInstanceState);
    				setContentView(R.layout.activity_main);
    		
    				// Comprobamos si estamos usando la version con
    				// con el FrameLayout
    				if (findViewById(R.id.fragment_container) != null) {
    					if (savedInstanceState != null) {
    						return;
    					}
    		
    					// Establecemos el ListFragment en el caso de que sea la version
    					// de un panel (SmartPhone)
    					TituloFragment fragment = new TituloFragment();
    					getSupportFragmentManager().beginTransaction()
    							.add(R.id.fragment_container, fragment, null).commit();
    				}
    			}
    		
    			@Override
    			public void onTituloSelected(int position) {
    				// TODO Auto-generated method stub
    		
    				// Comprobamos si tenemos disponible el Fragment de
    				// contenido
    				ContenidoFragment contFragment = (ContenidoFragment) getSupportFragmentManager()
    						.findFragmentById(R.id.contenidoFragment);
    		
    				if (contFragment != null) {
    					// Si está disponible, estamos en la versión de 2 paneles
    					contFragment.actualizarContenido(position);
    				} else {
    					// Si no está disponible, estamos en el layout
    					// del FrameLayout, y tenemos que cambiar los Fragment
    					contFragment = new ContenidoFragment();
    					Bundle args = new Bundle();
    		
    					// Establecemos la posición que hemos elegido
    					args.putInt(ContenidoFragment.POSICION, position);
    					contFragment.setArguments(args);
    		
    					// Reemplazamos el Fragment que había por el nuevo
    					getSupportFragmentManager().beginTransaction()
    							.replace(R.id.fragment_container, contFragment)
    							.addToBackStack(null).commit();
    				}
    			}
    		
    		}
    		

    Vamos a analizar todo parte por parte:

    • onCreate

      Este método pertenece al ciclo de vida de una Activity y es el primero en ejecutarse.

      				@Override
      				protected void onCreate(Bundle savedInstanceState) {
      					super.onCreate(savedInstanceState);
      					setContentView(R.layout.activity_main);
      			
      					// Comprobamos si estamos usando la version con
      					// con el FrameLayout
      					if (findViewById(R.id.fragment_container) != null) {
      						if (savedInstanceState != null) {
      							return;
      						}
      			
      						// Establecemos el ListFragment en el caso de que sea la version
      						// de un panel (SmartPhone)
      						TituloFragment fragment = new TituloFragment();
      						getSupportFragmentManager().beginTransaction()
      								.add(R.id.fragment_container, fragment, null).commit();
      					}
      				}
      				

      Lo primero que hacemos es comprobar si existe un View llamado “fragment_container”. Esta View corresponde al FrameLayout de nuestro activity_main en la carpeta res/layout. Cuando ejecutamos la aplicación en un SmartPhone se comprobará el layout que se ha elegido, si estamos usando este layout, y la View con id “fragment_container” está disponible, quiere decir que estamos usando la versión en la que solo mostramos un Fragment a la vez, por tanto establecemos el ListFragment que hemos denominado TituloFragment.

      En el caso de que se ejecutara en una tablet, el resultado de la comprobación con el método findViewById sería null, y por tanto los Fragment ya estarían colocados, como hemos hecho en el layout para Tablets.

    • onTituloSelected

      Este método tenemos que implementarlo cuando nuestra Activity implementa la interface de nuestro TituloFragment. Aquí trataremos también si se está ejecutando la versión SmartPhone, en el cual deberemos reemplazar el Fragment que ya existe por el otro, o la versión de Tablet, en el cuál le mandaremos un extra con la posición.

      				@Override
      				public void onTituloSelected(int position) {
      					// TODO Auto-generated method stub
      			
      					// Comprobamos si tenemos disponible el Fragment de
      					// contenido
      					ContenidoFragment contFragment = (ContenidoFragment) getSupportFragmentManager()
      							.findFragmentById(R.id.contenidoFragment);
      			
      					if (contFragment != null) {
      						// Si está disponible, estamos en la versión de 2 paneles
      						contFragment.actualizarContenido(position);
      					} else {
      						// Si no está disponible, estamos en el layout
      						// del FrameLayout, y tenemos que cambiar los Fragment
      						contFragment = new ContenidoFragment();
      						Bundle args = new Bundle();
      			
      						// Establecemos la posición que hemos elegido
      						args.putInt(ContenidoFragment.POSICION, position);
      						contFragment.setArguments(args);
      			
      						// Reemplazamos el Fragment que había por el nuevo
      						getSupportFragmentManager().beginTransaction()
      								.replace(R.id.fragment_container, contFragment)
      								.addToBackStack(null).commit();
      					}
      				}
      				

      Lo primero que hacemos es comprobar si tenemos disponible un ContenidoFragment disponible. En el caso de que esté disponible quiere decir que estamos usando la versión de 2 paneles (Tablet), por tanto lo único que haremos es llamar a su método actualizarContenido pasandole un entero, que es la posición que hemos recibido como argumento.

      En caso contrario, estamos en la versión de 1 panel únicamente a la vez (SmartPhone) por lo que deberemos reemplazar el Fragment que ya existe por otro. Lo que haremos es instanciar el Fragment, crear un objeto de la clase Bundle para pasarle la posición y agregarselo. A continuación hacemos uso del método getSupportFragmentManager() para comenzar una transacción en la que reemplazaremos el contenido que ya existe por el nuevo Fragment. Luego hacemos uso del método addToBackStack() para poder volver al Fragment anterior y completamos el cambio con el método commit().

Con esto tendríamos ya nuestra aplicación funcionando tanto en Tablets como en SmartPhones con un comportamiento diferente debido a sus tipos de pantalla.

Vemos a continuación una serie de capturas de pantalla y un vídeo en el que vemos la diferencia entre ambos dispositivos.

PrimerElementoSmartPhoneSegundoElementoSmartPhoneTercerElementoSmartPhoneCuartoElementoSegundoElemetnoTercerElementoPrimerElemento

Unos vídeos de los diferentes emuladores ejecutando la misma aplicación:

Ejemplo en Tablet:


Ejemplo en SmartPhone:



Con esta entrada hemos visto como adaptar nuestras aplicaciones tanto a dispositivos Tablet como SmartPhone por medio del uso de Fragment. Al principio puede ser algo complejo pero todo es tener claro lo que queremos hacer para hacerlo sin dudas y sin tener que cambiar la mitad de la aplicación mas adelante.

Este ejemplo es muy parecido al que se encuentra en la página web de Android Developer, el cuál es el siguiente: http://developer.android.com/training/basics/fragments/communicating.html . Comov eremos es prácticamente igual, ya que llegado a cierto punto todo termina pareciendose cuando tienen la misma funcionalidad.

El código usado en el ejemplo podéis descargarlo de aquí y está disponible también en GitHub: aquí

Sin más, cualquier aporte o corrección es bienvenido.

Saludos!!!

  • Juan Urrutia

    Hola que tal, veo que el código, asi como comentaste, es el ejemplo que viene en http://developer.android.com/guide/components/fragments.html el cual ya he entendido muy bien.

    Solo tengo una duda, en el ejemplo solo se cambia el Fragment de contenido, actualizando su Texto, pero, ¿hay forma de que en lugar de cambiar solo el texto, se pusiera otra Activity?

  • Buenas Juan,

    SI mal no creo, y si es así perdona, pero creo que Android no permite mostrar 2 Activity a la vez. Es por ello que se usan los Fragment.

    Lo que sí puedes hacer es cambiar ese Fragment completamente por otro que tengas (que creo que es a lo que te refieres). Es decir, en vez de usar un Fragment con un TextView, abrir uno que tiene a lo mejor un formulario, o una foto o cualquier otro tipo de Fragment.

    Si es así solo debes crear un Fragment con los elementos que desees y controlar eso en la Activity.

    Espero que te sea de ayuda!

    Saludos!!

  • Txuribo

    Buenas David,

    El articulo esta muy bien para la gente que estamos empezando en este mundillo y te agradezco tu esfuerzo, pero me ha quedado una duda de novato. Al implementar setListAdapater he visto que haces uso de “Contenido.titulos”, donde lo tienes definido? No lo he visto en el código, te dejo aquí el cacho de código donde aparece.

    setListAdapter(new ArrayAdapter(getActivity(),android.R.layout.simple_list_item_1, Contenido.titulos));

    Un saludo y gracias!!