Guía de Desarrolladores de Android para el Patrón de Navegación de Fragmentos
Con el pasar de los años he visto diferentes implementaciones del patrón de navegación de Android. Algunas aplicaciones usaban solo Actividades, mientras que otras Actividades se mezclaban con Fragmentos y/o con Vistas Personalizadas (Custom Views).
Una de mis implementaciones de patrón de fragmentos favorita está basada en la filosofía “Una-Actividad-Múltiples-Fragmentos” (“One-Activity-Multiple-Fragments”), o sencillamente el Patrón de Navegación de Fragmentos (Fragment Navigation Pattern), en donde cada pantalla en la aplicación es un Fragmento de pantalla completa y todos o la mayoría de estos fragmentos se encuentran dentro de una Actividad.
Este enfoque no solo simplifica como se implementa la navegación, sino que también tiene un mejor rendimiento y por ende le ofrece una mejor experiencia de usuario.
En este artículo exploraremos algunas implementaciones de patrón de navegación comunes en Android, luego introduciremos el patrón de navegación basado en Fragmentos. Una aplicación de muestra que implementa este patrón puede ser encontrada en GitHub.
Un Mundo de Actividades
Una aplicación de Android típica que utiliza solamente actividades está organizada en una estructura de tipo Árbol (precisamente en una grafo dirigido) donde la actividad principal en iniciada por el launcher. Mientras navegas por la aplicación hay un “back stack” de la actividad que se mantiene gracias el Sistema Operativo.
Un ejemplo sencillo se muestra en el siguiente diagrama:
Actividad A1 es el punto de entrada de nuestra aplicación (por ejemplo, representa una pantalla inicial o un menú principal) y desde ese punto el usuario puede navegar hacia A2 o A3. Cuando necesites comunicarte entre actividades puedes utilizar startActivityForResult() o podrías compartir un objeto lógico de negocios globalmente accesible entre ambas actividades.
Cuando necesites agregar una nueva Actividad deberás seguir los siguientes pasos:
- Define la nueva actividad
- Regístrala enxml
- Ábrela con unstartActivity() de otra actividad
Aunque este diagrama de navegación es un enfoque bastante simplificado; pero puede volverse muy complicado cuando necesitas manipular de back stack o cuando tienes que re-utilizar la misma actividad varias veces, por ejemplo cuando te gustaría que el usuario navegará a través de diferentes pantallas de tutoriales pero cada pantalla utiliza la misma actividad como base.
Por suerte tenemos herramientas para estos casos llamadas tareas y algunas guías para una navegación de back stack apropiada.
Luego, con API nivel 11 llegaron los fragmentos…
Un Mundo de Fragmentos
“Android introduce los fragmentos en Android 3.0 (nivel de API 11), principalmente para admitir diseños de IU más dinámicos y flexibles en pantallas grandes, como las de las tablets. Como la pantalla de una tablet es mucho más grande que la de un teléfono, hay más espacio para combinar e intercambiar componentes de la IU. Los fragmentos admiten esos diseños sin la necesidad de que administres cambios complejos en la jerarquía de vistas. Al dividir el diseño de una actividad en fragmentos, puedes modificar el aspecto de la actividad durante el tiempo de ejecución y conservar esos cambios en una pila de actividades administrada por la actividad.” – cita de la Guía de Google API para Fragmentos.
Este Nuevo juguete permitió a los desarrolladores crear una Interfaz de Usuario multi-paneles y poder re-usar los componentes en otras actividades. Algunos desarrolladores adoran esto mientras que otros no tanto. Es un debate popular el hecho de usar o no Fragmentos, pero yo creo que todos estarán de acuerdo en que los fragmentos trajeron una complejidad adicional y los desarrolladores tienen que entenderlos apropiadamente para poder usarlos.
La Pesadilla del Fragmento de Pantalla Completa en Android
Comencé a ver más y más ejemplos donde los fragmentos no sólo representaban una parte de la pantalla, sino que toda la pantalla era un fragmento dentro de una actividad. Hubo una vez que vi un diseño en el que cada actividad tenía exactamente un fragmento de pantalla completa y nada más, y la única razón por la que esas actividades existían era para almacenar los fragmentos. Aparte del fallo en el diseño, hay otro problema con este enfoque. Mira un momento el diagrama a continuación:
¿Cómo puede A1 comunicarse con F1? Lo que sucede es que A1 tiene total control sobre F1, debido a que creó F1. A1 podría pasar un paquete, por ejemplo, en la creación de F1 o puede invocar sus métodos públicos. ¿Cómo puede F1 comunicarse con A1? Bueno eso es más complicado, puede resolverse con un patrón decallback/observer donde A1 se suscribe a F1 y F1 notifica a A1.
¿Pero cómo se pueden comunicar A1 y A2? Como se explicó anteriormente, se podrían comunicar vía startActivityForResult().
Y ahora la verdadera pregunta: ¿Cómo se pueden comunicar F1 y F2? Aún en este caso podemos tener un componente lógico de negocios el cual es accesible globalmente, y puede ser utilizado para pasar datos. Pero ese componente no siempre equivale a un diseño elegante. ¿Qué pasaría si F2 necesita pasarle información a F1 de forma más directa? En tal caso, con un patrón callback F2 puede notificar a A2, luego A2 termina con un resultado y ese resultado puede ser almacenado por A1 quien puede notificar a F1.
Este enfoque requiere de mucho código boilerplate y se convierte rápidamente en una fuente de bugs, dolor y rabia.
¿Y si pudiéramos deshacernos de todas las actividades y quedarnos sólo con una de ellas la cual mantendrá el resto de los fragmentos?
Patrón de Navegación de Fragmentos
Con el pasar del tiempo comencé a utilizar el patrón de “Una-Actividad-Múltiples-Fragmentos” en la mayoría de mis aplicaciones y aún lo sigo usando. Existen muchas discusiones por ahí sobre este enfoque o filosofía, por ejemplo aquí y aquí. Lo que me perdí fue un ejemplo en concreto que puedo ver y probar por mí mismo.
Miremos el siguiente diagrama un momento:
Ahora tenemos solo una actividad contenedora y tenemos fragmentos múltiples que de nuevo se encuentran en una estructura de tipo Árbol. La navegación entre ellos se maneja por el FragmentManager, este tiene su back stack.
Te podrás cuenta que ahora no tenemos el startActivityForResult() pero podemos implementar el patrón callback/observer. Ahora veamos algunos pros y contras de este enfoque:
Pros:
1. Más limpio y mantenible AndroidManifest.xml
Ahora que tenemos sólo una Actividad, ya no tenemos que actualizar el manifiesto cada vez que agregamos una nueva pantalla. A diferencia de las actividades, no tenemos que declarar los fragmentos.
Esto podría parecer una cosa pequeña, pero para aplicaciones más grandes con más de 50 actividades esto podría mejorar significativamente la legibilidad del AndroidManifest.xml file.
Observa el archivo del manifiesto de la aplicación ejemplo, el cual tiene varias pantallas. El archivo de manifiesto se mantiene súper sencillo.
<?xml version="1.0" encoding="utf-8"?> package="com.exarlabs.android.fragmentnavigationdemo.ui" > <application android:name= ".FragmentNavigationDemoApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name="com.exarlabs.android.fragmentnavigationdemo.ui.MainActivity" android:label="@string/app_name" android:screenOrientation="portrait" android:theme="@style/AppTheme.NoActionBar" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
2. Gestión centralizada de la navegación
En mi código ejemplo, notarás que use NavigationManager el cual en mi caso, es inyectado a cada uno de los fragmentos. Éste gestor puede ser usado como un lugar central para el logging, gestión back stack entre otras, así que los comportamientos de navegación se desacoplan del resto de la lógica del negocio y no se esparcen en implementaciones de diferentes pantallas.
Imaginemos una situación en la que queremos iniciar una pantalla, donde el usuario puede seleccionar algunos ítems de una lista de personas. También te gustaría pasar algunos argumentos de filtrado, como edad, ocupación y género.
En caso de Actividades, escribirías:
Intent intent = new Intent(); intent.putExtra("age", 40); intent.putExtra("occupation", "developer"); intent.putExtra("gender", "female"); startActivityForResult(intent, 100);
Luego, debes definir onActivityResult en algún lugar abajo y manejar el resultado.
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); }
Mi problema personal con éste acercamiento es que estos argumentos son “extras” y no son obligatorios, así que tengo que asegurarme que la actividad receptora maneje los diferentes casos, cuando falte un extra. Después cuando se realice una refactorización y cuando el extra, por ejemplo, de “edad” ya no sea necesario, entonces tengo que buscar por todo el código donde se inició esta actividad y asegurarme de que todos los extras sean correctos.
Además, ¿No sería mejor si el resultado (lista de personas) llegará como _List_ y no en una forma serial que luego debe ser deserializada?
En el caso de la navegación basada en fragmentos, todo es más directo. Todo lo que tienes que hacer es escribir un método en el NavigationManager llamado startPersonSelectorFragment() con los argumentos necesarios y con una implementación callback.
mNavigationManager.startPersonSelectorFragment(40, "developer", "female", new PersonSelectorFragment.OnPersonSelectedListener() { @Override public boolean onPersonsSelected(List<Person> selection) { [do something] return false; } });
O con RetroLambda
mNavigationManager.startPersonSelectorFragment(40, "developer", "female", selection -> [do something]);
3. Mejores formas de comunicación entre pantallas
Entre actividades, solo podemos compartir un Paquete que contenga datos primitivos o serializados. Ahora con fragmentos podemos implementar un patrón callback donde, por ejemplo, F1 puede escuchar a F2 pasando objetos arbitrarios. Por favor, dale un vistazo a los ejemplos anteriores de implementación de callback, which returns back a _List_.
4. Crear Fragmentos es menos costoso que crear Actividades
Esto se vuelve obvio cuando usas un cajón que tiene por ejemplo 5 ítems de menú y en cada página el cajón debería ser mostrado nuevamente.
En caso de navegación de actividad pura, cada página debería inflarse e iniciar el cajón, pero por supuesto esto es caro.
En el diagrama que se muestra puedes ver varios fragmentos de raíz o root fragments (FR*) los cuales son los fragmentos de pantalla que pueden ser accedidos directamente desde el cajón, y también el cajón es solo accesible cuando se muestran estos fragmentos. Todo lo que se encuentra a la derecha de la línea marcada en el diagrama está ahí como ejemplo de un esquema de navegación arbitraria.
Ya que la actividad contenedora contiene el cajón, solo tenemos una instancia de cajón, por lo que cada paso de navegación donde el cajón debería ser visible no tienes que iniciarlo nuevamente. ¿Aún no estás convencido de cómo funciona todo? Dale un vistazo a mi aplicación de muestra donde se demuestra el uso de los cajones.
Contras
Mi gran temor siempre ha sido que si utilizo el patrón de navegación de fragmentos en un proyecto, entonces en algún momento encontraré algún problema desconocido que será difícil de resolver alrededor de la complejidad de los fragmentos, las bibliotecas de terceros y las diferentes versiones de Sistema Operativo. ¿Y si tuviera que refractar todo lo que he hecho hasta ahora?
De hecho, tendría que resolver los problemas con nested fragments, bibliotecas de terceros las cuales utilizan fragmentos como ShinobiControls, ViewPagers y FragmentStatePagerAdapters.
Admito que obtener la experiencia suficiente con fragmentos como para resolver estos problemas fue un largo proceso. Pero en cada caso el problema no era que la filosofía es mala, sino que no entendía fragmentos lo suficiente. Pero si entiendes fragmentos mejor que yo en aquel momento entonces no tendrás ningún problema.
El único contra que puedo mencionar ahora es que podemos encontrar problemas que no sean triviales de resolver, debido a que no hay una biblioteca madura que muestre todos los escenarios complejos de una aplicación compleja con navegación basada en fragmentos.
Conclusión
En este artículo, hemos visto una alternativa para implementar navegación en una aplicación Android . Se comparó el patrón con la filosofía de navegación tradicional que utiliza actividades y vimos algunas muy buenas razones por las que es ventajoso usar fragmentos en lugar del enfoque tradicional.
En caso de que no lo hayas revisado aún, chequea la aplicación demo en la implementación GitHub . No temas contribuir en la misma con buenos ejemplos que podrían demostrar de mejor forma su uso.
Artículo Vía: Toptal