Navegando entre fragmentos sin perder su estado | Android

Carlo Huamán
OrbisMobile
Published in
7 min readJun 4, 2018
vía CNN | Anisha Shah

Más de una vez me he topado con el dilema de: ¿Cómo debo cambiar entre fragmentos al implementar Tabs o un Bottom navigation en una Actividad?, hay muchas formas de hacerlo en realidad, pero siempre esta la duda. ¿Cómo?

via Giphy

En realidad esto no es muy complicado, basta con usar un replace y ¡Listo! ¿no?. Creo que todos alguna vez, cuando hemos querido cambiar entre fragmentos hemos hecho algo así:

val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.container, fragment)
transaction.commit()

Y de hecho esta bien, funciona. Y si lo lleváramos a una simple aplicación, tendriamos algo asi:

Pues todo bien hasta ahí mientras avancemos de un fragmento a otro, pero ¿Y si quisiera regresar al fragmento anterior?

Ups!

¿Qué sucedió?, ¿Porqué no regresé al anterior fragmento? — Al presionar el botón atrás (back) de nuestro dispositivo, accionamos un evento llamado onBackPressed. Este evento te permite “retroceder/navegar”, entre las actividades que hallas agregado a la pila de la aplicación (task).

Hagamos una pausa aquí para entender primero que sucede con la pila de una aplicación.

BackStack Diagram — vía Android Developers

Como podemos ver en la imagen anterior, primero sólo tenemos un Activty 1, como si hubiéramos creado una aplicación que tenga Menú Principal (Activity 1), luego nos mande a un resultado de búsquedas (Activity 2), y al picar en un resultado, nos dirijamos a un detalle de este resultado(Activity 3).

Hemos creado un flujo de Activty 1 → Activity 2 → Activity 3, también llamado pila (task) de nuestra aplicación. Si volvemos atrás por medio del evento onBackPressed, el Activity 3 es cerrado/terminado, y la aplicación trae al último elemento de la pila, en este caso el Activity 2.

BackStack Activities | vía YouTube Android Developers

Todo bien, pero ¿Y con los fragmentos que sucede?. Los fragmentos también pueden ser agregados a la pila agregando una línea a nuestro código inicial.

val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.container, fragment)
transaction.addToBackStack(null)
transaction.commit()
BackStack Fragments | vía YouTube Android Developers

Entonces, si mis fragmentos ya son agregados a la pila de la aplicación ya puedo navegar entre ellos ¿verdad? Veamos como quedaría nuestra aplicación ahora.

Yeah!

Entonces, ya puedo navegar entre fragmentos 🙌🏻💪🏽🤘🏽, sin embargo, ¿Qué sucede detrás del replace() que usamos para cambiar los fragmentos?; veamos que nos dice la documentación:

Reemplaza un fragmento existente que halla sido agregado a un contenedor. Esto es esencialmente lo mismo que llamar a un remove(Fragment) para todos los fragmentos que fueron agregados con el mismo containerViewId y luego un add(int, Fragment, String) con los mismos argumentos dados aquí. (developer.android.com)

Entonces, un replace() reemplaza (valga la redundancia 😆) un fragmento existente en el contenedor que estemos trabajando. Para lograr esto, invoca dos funciones de forma interna: remove() y add(); ¿Qué dice la documentación acerca de un remove()?:

Elimina un fragmento existente. Si se agregó a un contenedor, su vista también se elimina de ese contenedor. (developer.android.com)

¿Por qué es importante saber todo esto si mi aplicación ya esta funcionando?. Hagámonos las siguientes preguntas:

  • ¿Qué pasaría si en lugar de tener un pequeño texto en mi fragmento tengo un ListView o RecyclerView?
  • ¿Qué pasaría si cargo datos de un servicio externo (REST | GraphQL)?
  • ¿Qué pasaría si tengo un ScrollView del cual quiero mantener su última posición?
  • ¿Si sólo voy a manejar entre 3 a 4 fragmentos, tengo que estar recreándolos todos a cada momento que cambio entre ellos mismos?

Se que hay muchos tutoriales que nos muestran que jugando con el onBackStackChanged() se puede manipular el cambio entre fragmentos, pero personalmente es una forma que no me gusta usar, más aún si quiero mantener el estado de mis fragmentos sin tener que cargar todo desde 0 o estar agregando más información a la pila de la aplicación. Entonces, ¿que más puedo hacer?

hide() & show()

El FragmentTransaction que nos permite acceder al método replace que maneja el cambio de nuestros fragmentos, maneja también una serie de operaciones como attach, detach, show, hide, add, isEmpty, etc; pero nos enfocaremos en los métodos hide and show.

Hide

Oculta un fragmento existente. Esto sólo es relevante para los fragmentos cuyas vistas han sido añadidas a un contenedor, ya que esto hará que se oculte la vista.(Android Developers)

Show

Muestra un fragmento que esté oculto. Esto sólo es relevante para los fragmentos cuyas vistas han sido añadidas a un contenedor, ya que esto hará que se muestre la vista. (Android Developers)

Entonces ahora, en lugar de destruir el fragmento y volverlo a recrear con un replace(), simplemente lo ocultaremos con un hide() y/o mostraremos con un show(). Ahora nuestro código se vería de la siguiente forma:

val transaction = supportFragmentManager.beginTransaction()

if (fragment.isAdded) {
transaction
.hide(currentFragment)
.show(fragment)
} else {
transaction
.hide(currentFragment)
.add(R.id.container, fragment, tag)
}

transaction.commit()

Como podemos ver, el mismo transaction que usábamos antes para usar el replace(), ahora oculta el fragmento actual (currentFragment) y muestra el que queramos mostrar (de nuevo valga la redundancia 😆) sin necesidad de usar el replace(), obviamente tenemos que asegurarnos que el fragmento a mostrar ya este agregado, ello lo hacemos con la propiedad isAdded, caso contrario lo agregamos con un add();

Una diferencia respecto a nuestra anterior implementación es que usamos un tag cada vez que agregamos un nuevo fragmento, y esto es en caso necesitemos recuperar un fragmento específico en un momento dado, lo podemos encontrar por su tag. Sin embargo observamos que ya no usamos el método addToBackStack(tag), entonces ¿Cómo recuperamos nuestro anterior fragmento si deseo navegar entre ellos?.

override fun onBackPressed() {
.
.
recoverFragment()
.
.
}

Si el usuario usara el botón atrás para retroceder a su anterior fragmento o cree un botón que haga eso, podemos mover esa lógica a una función que me permita devolver el anterior fragmento en el cual estuve, y ¿Cómo lo recuperaría?.

val transaction = supportFragmentManager.beginTransaction()

val currentFragment = supportFragmentManager.findFragmentByTag(currentFragmentTag)
val oldFragment = supportFragmentManager.findFragmentByTag(oldFragmentTag)

if (currentFragment.isVisible && oldFragment.isHidden) {
transaction.hide(currentFragment).show(oldFragment)
}

transaction.commit()

Como pueden ver en el anterior código, primero recupero los fragmentos a mostrar/ocultar con ayuda del método findFragmentByTag(),en base a los tags que ya les asigné anteriormente. Lo siguiente a trabajar es ocultar o mostrar los fragmentos hallados, siempre verificando si están visibles o no con ayuda de los métodos “isVisible y isHidden”; todo esto debido a que el usuario quiera mostrar una vista que ya se esté mostrando o al revés, quiera ocultar una vista que ya esté oculta, a fin de evitar transacciones y errores innecesarios.

BONUS

Navegación en un Bottom Navigation

Hablando de Bottom Navigation como menú en las aplicaciones, ¿Han visto el comportamiento de la barra de navegación de YouTube o Spotify?. Ambas son dos caras de la moneda, mientras el comportamiento de YouTube va de un histórico máximo de 5 tabs y luego home y exit; Spotify retrocede infinitamente todo el histórico que hallas hecho. Ambas tiene su pro y su contra, sin embargo comparten algo común, es que ambas respetan el estado de cada fragmento, es decir, si en una pantalla X hiciste scroll hasta la mitad de una lista o dejaste ciertos controles activados, etc; cuando regreses a ese fragmento, estará tal cual lo has dejado. ¿Como podríamos replicar este comportamiento?

when (listState.size) {
MAX_HISTORIC -> {

...

for (i in listState.indices) {
if (listState.indices.contains((i + 1))) {
listState[i] = listState[i + 1]
}
}
...
}
else -> {
listState.add(StateFragment(currentFragmentTag, oldFragmentTag))
}
}

Creamos una lista donde agregas los estados de cada fragmento que quieras conservar, recuperar. Y luego en base a una lógica que definas, vas recuperando de tu lista los fragmentos que necesites, simulando tu propio Back Stack. En mi caso uso una variable MAX_HISTORIC donde defino el máximo histórico que deseo conservar. Sumado esto a la lógica que hemos armado en el presente post, tendremos un cambio entre nuestros fragmentos sin perder sus estados.

El código completo de este proyecto esta en el siguiente repositorio:

Referencias

--

--

Carlo Huamán
OrbisMobile

#Jesus is the way, the truth and the life | Dad, Mobile Architect @ BCP, Assistant #GDE | 🚴🥭🍫🍊 | bio.link/tohure