Guía del desarrollador de Android para el patrón de navegación de fragmentos

Publicado: 2022-03-11

A lo largo de los años, he visto muchas implementaciones de patrones de navegación diferentes en Android. Algunas de las aplicaciones usaban solo actividades, mientras que otras actividades se mezclaban con fragmentos y/o con vistas personalizadas.

Una de mis implementaciones de patrones de navegación favoritas se basa en la filosofía de "una actividad, múltiples fragmentos", o simplemente el patrón de navegación de fragmentos, donde cada pantalla de la aplicación es un fragmento de pantalla completa y todos o la mayoría de estos fragmentos están contenidos en una Actividad.

Este enfoque no solo simplifica la forma en que se implementa la navegación, sino que tiene un rendimiento mucho mejor y, en consecuencia, ofrece una mejor experiencia de usuario.

En este artículo, veremos algunas implementaciones de patrones de navegación comunes en Android y luego presentaremos el patrón de navegación basado en fragmentos, comparándolo y contrastándolo con los demás. Se ha subido a GitHub una aplicación de demostración que implementa este patrón.

Mundo de actividades

Una aplicación típica de Android que usa solo actividades se organiza en una estructura similar a un árbol (más precisamente en un gráfico dirigido) donde el iniciador inicia la actividad raíz. A medida que navega en la aplicación, hay una pila de actividad mantenida por el sistema operativo.

Un ejemplo simple se muestra en el siguiente diagrama:

Fragmento de imagen 1

La actividad A1 es el punto de entrada en nuestra aplicación (por ejemplo, representa una pantalla de inicio o un menú principal) y desde allí el usuario puede navegar a A2 o A3. Cuando necesite comunicarse entre actividades, puede usar startActivityForResult() o tal vez compartir un objeto de lógica de negocios accesible globalmente entre ellos.

Cuando necesite agregar una nueva actividad, debe realizar los siguientes pasos:

  • Definir la nueva actividad.
  • Registrarlo en el AndroidManifest.xml
  • Ábralo con un startActivity() de otra actividad

Por supuesto, este diagrama de navegación es un enfoque bastante simplista. Puede volverse muy complejo cuando necesita manipular la pila de actividades o cuando tiene que reutilizar la misma actividad varias veces, por ejemplo, cuando le gustaría navegar al usuario a través de algunas pantallas de tutoriales pero cada pantalla, de hecho, usa la misma actividad como un base.

Afortunadamente, tenemos herramientas para ello llamadas tareas y algunas pautas para una navegación adecuada en la pila trasera.

Luego, con el nivel 11 de API llegaron los fragmentos...

mundo de fragmentos

Android introdujo fragmentos en Android 3.0 (API nivel 11), principalmente para admitir diseños de interfaz de usuario más dinámicos y flexibles en pantallas grandes, como tabletas. Debido a que la pantalla de una tableta es mucho más grande que la de un teléfono, hay más espacio para combinar e intercambiar componentes de la interfaz de usuario. Los fragmentos permiten tales diseños sin la necesidad de administrar cambios complejos en la jerarquía de vistas. Al dividir el diseño de una actividad en fragmentos, puede modificar la apariencia de la actividad en el tiempo de ejecución y conservar esos cambios en una pila de actividades administrada por la actividad. – citado de la guía API de Google para fragmentos.

Este nuevo juguete permitió a los desarrolladores crear una interfaz de usuario de varios paneles y reutilizar los componentes en otras actividades. A algunos desarrolladores les encanta esto, mientras que a otros no. Es un debate popular si usar fragmentos o no, pero creo que todos estarían de acuerdo en que los fragmentos aportaron una complejidad adicional y que los desarrolladores realmente necesitan entenderlos para usarlos correctamente.

Pesadilla de fragmentos a pantalla completa en Android

Empecé a ver más y más ejemplos en los que los fragmentos no solo representaban una parte de la pantalla, sino que, de hecho, toda la pantalla era un fragmento contenido en una actividad. Una vez incluso 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 existían estas actividades era para alojar estos fragmentos. Junto a la obvia falla de diseño, hay otro problema con este enfoque. Echa un vistazo al diagrama de abajo:

Fragmento de imagen 2

¿Cómo puede A1 comunicarse con F1? Bueno, A1 tiene control total sobre la F1 desde que creó la F1. A1 puede 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, esto es más complicado, pero se puede resolver con un patrón de devolución de llamada/observador en el que A1 se suscribe a F1 y F1 notifica a A1.

Pero, ¿cómo pueden comunicarse A1 y A2 entre sí? Esto ya se ha cubierto, por ejemplo, a través de startActivityForResult() .

Y ahora viene la verdadera pregunta: ¿cómo pueden F1 y F2 comunicarse entre sí? Incluso en este caso, podemos tener un componente de lógica empresarial que esté disponible globalmente, por lo que puede usarse para pasar datos. Pero esto no siempre conduce a un diseño elegante. ¿Qué pasa si F2 necesita pasar algunos datos a F1 de una manera más directa? Bueno, con un patrón de devolución de llamada, F2 puede notificar a A2, luego A2 termina con un resultado y A1 captura este resultado y notifica a F1.

Este enfoque necesita mucho código repetitivo y rápidamente se convierte en una fuente de errores, dolor e ira.

¿Qué pasaría si pudiéramos deshacernos de todas las actividades y mantener solo una de ellas que conserve el resto de los fragmentos?

Patrón de navegación de fragmentos

A lo largo de los años, comencé a usar el patrón "Una actividad, múltiples fragmentos" en la mayoría de mis aplicaciones y todavía lo uso. Hay muchas discusiones sobre este enfoque, por ejemplo aquí y aquí. Sin embargo, lo que me perdí es un ejemplo concreto que puedo ver y probar.

Echemos un vistazo al siguiente diagrama:

imagen Framgnet 3

Ahora solo tenemos una actividad de contenedor y tenemos múltiples fragmentos que nuevamente tienen una estructura similar a un árbol. La navegación entre ellos es manejada por FragmentManager , tiene su back stack.

Tenga en cuenta que ahora no tenemos startActivityForResult() pero podemos implementar un patrón de devolución de llamada/observador. Veamos algunos pros y contras de este enfoque:

Ventajas:

1. AndroidManifest.xml más limpio y fácil de mantener

Ahora que solo tenemos una Actividad, ya no necesitamos actualizar el manifiesto cada vez que agregamos una nueva pantalla. A diferencia de las actividades, no tenemos que declarar fragmentos.

Esto puede parecer algo menor, pero para aplicaciones más grandes que tienen más de 50 actividades, esto puede mejorar significativamente la legibilidad del archivo AndroidManifest.xml .

Mire el archivo de manifiesto de la aplicación de ejemplo que tiene varias pantallas. El archivo de manifiesto sigue siendo súper simple.

 <?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 de navegación centralizada

En mi ejemplo de código, verá que uso NavigationManager , que en mi caso se inyecta en cada fragmento. Este administrador se puede usar como un lugar centralizado para el registro, la administración de back stack, etc., de modo que los comportamientos de navegación se desvinculen del resto de la lógica comercial y no se distribuyan en implementaciones de diferentes pantallas.

Imaginemos una situación en la que nos gustaría iniciar una pantalla donde el usuario puede seleccionar algunos elementos de una lista de personas. También le gustaría pasar algunos argumentos de filtrado como la edad, la ocupación y el género.

En el caso de Actividades, se escribiría:

 Intent intent = new Intent(); intent.putExtra("age", 40); intent.putExtra("occupation", "developer"); intent.putExtra("gender", "female"); startActivityForResult(intent, 100);

Luego, debe definir onActivityResult en algún lugar debajo y manejar el resultado.

 @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); }

Mi problema personal con este enfoque es que estos argumentos son "extras" y no son obligatorios, por lo que debo asegurarme de que la actividad de recepción maneje todos los casos diferentes cuando falta un extra. Más tarde, cuando se realiza alguna refactorización y ya no se necesita el extra de "edad", por ejemplo, entonces tengo que buscar en todas partes del código donde comienzo esta actividad y asegurarme de que todos los extras sean correctos.

Además, ¿no sería mejor si el resultado (lista de personas) llegara en forma de _Lista _ y no en una forma serializada que debe ser deserializado?

En el caso de la navegación basada en fragmentos, todo es más sencillo. Todo lo que tiene que hacer es escribir un método en el NavigationManager llamado startPersonSelectorFragment() con los argumentos necesarios y con una implementación de devolución de llamada.

 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 medios de comunicación entre pantallas

Entre actividades, podemos compartir solo un paquete que puede contener primitivos o datos serializados. Ahora, con fragmentos, podemos implementar un patrón de devolución de llamada donde, por ejemplo, F1 puede escuchar a F2 pasando objetos arbitrarios. Eche un vistazo a la implementación de devolución de llamada de los ejemplos anteriores, que devuelve una _List _.

4. Los fragmentos de construcción son menos costosos que las actividades de construcción

Esto se vuelve obvio cuando usa un cajón que tiene, por ejemplo, 5 elementos de menú y en cada página el cajón debe mostrarse nuevamente.

En caso de navegación de actividad pura, cada página debería inflar e inicializar el cajón, lo que por supuesto es costoso.

En el siguiente diagrama, puede ver varios fragmentos raíz (FR*), que son los fragmentos de pantalla completa a los que se puede acceder directamente desde el cajón, y solo se puede acceder al cajón cuando se muestran estos fragmentos. Todo lo que está a la derecha de la línea discontinua en el diagrama está ahí como un ejemplo de un esquema de navegación arbitrario.

imagen framgnet 4

Dado que la actividad del contenedor contiene el cajón, solo tenemos una instancia de cajón, por lo que en cada paso de navegación en el que debería estar visible el cajón no tiene que inflarlo e inicializarlo nuevamente. ¿Aún no estás convencido de cómo funcionan todos estos? Eche un vistazo a mi aplicación de muestra que demuestra el uso del cajón.

Contras

Mi mayor temor siempre había sido que si usaba un patrón de navegación basado en fragmentos en un proyecto, en algún momento me encontraría con un problema imprevisto que sería difícil de resolver debido a la complejidad adicional de los fragmentos, las bibliotecas de terceros y las diferentes versiones del sistema operativo. ¿Qué pasaría si tuviera que refactorizar todo lo que he hecho hasta ahora?

De hecho, tuve que resolver problemas con fragmentos anidados, bibliotecas de terceros que también usan fragmentos como ShinobiControls, ViewPagers y FragmentStatePagerAdapters.

Debo admitir que ganar suficiente experiencia con fragmentos para poder resolver estos problemas fue un proceso bastante largo. Pero en todos los casos el problema no era que la filosofía fuera mala, sino que no entendía los fragmentos lo suficientemente bien. Tal vez si entiendes los fragmentos mejor que yo, ni siquiera te encontrarías con estos problemas.

La única desventaja que puedo mencionar ahora es que aún podemos encontrar problemas que no serían triviales de resolver, ya que no existe 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 forma alternativa de implementar la navegación en una aplicación de Android. Lo comparamos con la filosofía de navegación tradicional que usa actividades y hemos visto algunas buenas razones por las que es ventajoso usarlo sobre el enfoque tradicional.

En caso de que aún no lo haya hecho, consulte la aplicación de demostración cargada en la implementación de GitHub. Siéntase libre de bifurcarlo o contribuir con ejemplos más agradables que mostrarían mejor su uso.

Relacionado: Los 10 errores más comunes que cometen los desarrolladores de Android