Administrador de fragmentos

FragmentManager es la clase responsable de realizar acciones en los fragmentos de tu app, como agregarlos, quitarlos o reemplazarlos, así como agregarlos a la pila de actividades.

Es posible que nunca interactúes con FragmentManager directamente si usas la biblioteca de Jetpack Navigation, ya que funciona con FragmentManager por ti. Dicho esto, cualquier app que utilice fragmentos usa FragmentManager en algún punto, por lo que es importante comprender qué es y cómo funciona.

En este tema, se explica cómo acceder al FragmentManager, cuál es la función del FragmentManager en relación con tus actividades y fragmentos, cómo administrar la pila de actividades con FragmentManager y cómo proporcionar datos y dependencias a tus fragmentos.

Accede a FragmentManager

Cómo acceder a una actividad

Todas las FragmentActivity y las subclases asociadas, como AppCompatActivity, tienen acceso a FragmentManager a través del método getSupportFragmentManager().

Cómo acceder a un fragmento

Los fragmentos también pueden alojar uno o más fragmentos secundarios. Dentro de un fragmento, puedes obtener una referencia a FragmentManager que administra los elementos secundarios del fragmento mediante getChildFragmentManager(). Si necesitas acceder a su host FragmentManager, puedes usar getParentFragmentManager().

Veamos algunos ejemplos que muestran las relaciones entre los fragmentos, sus hosts y las instancias del FragmentManager asociadas con cada uno.

dos ejemplos de diseño de la IU que muestran las relaciones entre los fragmentos y sus actividades de host
Figura 1: Dos ejemplos de diseño de la IU que muestran las relaciones entre los fragmentos y sus actividades de host

En la Figura 1, se muestran dos ejemplos, cada uno de los cuales tiene una sola actividad de host. La actividad de host de ambos ejemplos muestra la navegación de nivel superior al usuario como una BottomNavigationView encargada de reemplazar el fragmento de host con diferentes pantallas de la app, y cada pantalla se implementa como un fragmento separado.

El fragmento de host del Ejemplo 1 aloja dos fragmentos secundarios que conforman una pantalla con vista dividida. El fragmento de host del Ejemplo 2 aloja un solo fragmento secundario que conforma el fragmento de la pantalla de una vista deslizante.

Dada esta configuración, puedes pensar que cada host tiene un FragmentManager asociado que administra sus fragmentos secundarios. Esto se ilustra en la Figura 2, junto con las asignaciones de propiedades entre supportFragmentManager, parentFragmentManager y childFragmentManager.

cada host tiene su propio FragmentManager asociado que administra sus fragmentos secundarios
Figura 2: Cada host tiene su propio FragmentManager asociado que administra sus fragmentos secundarios

La propiedad FragmentManager adecuada a la cual hacer referencia depende del lugar en el que se encuentra el sitio de llamada en la jerarquía del fragmento, así como del administrador de fragmentos al que intentas acceder.

Una vez que tengas una referencia al FragmentManager, podrás usarla para manipular los fragmentos que se muestran al usuario.

Fragmentos secundarios

En términos generales, tu app debe incluir una o varias actividades en el proyecto de la aplicación, y cada actividad debe representar un grupo de pantallas relacionadas. La actividad puede proporcionar un punto para colocar la navegación de nivel superior y un lugar a fin de definir los ViewModels y otro estado de vista entre fragmentos. Cada destino individual de tu app debe estar representado por un fragmento.

Si deseas mostrar varios fragmentos a la vez, como en una vista dividida o un panel, debes usar fragmentos secundarios administrados por tu fragmento de destino y su administrador de fragmentos secundarios.

Otros casos de uso para los fragmentos secundarios pueden incluir los siguientes:

  • Diapositivas de pantalla, con un ViewPager2 en un fragmento superior para administrar una serie de vistas de fragmentos secundarios
  • Subnavegación dentro de un conjunto de pantallas relacionadas
  • Jetpack Navigation usa fragmentos secundarios como destinos individuales. Una actividad aloja un único NavHostFragment superior y llena su espacio con diferentes fragmentos secundarios de destino a medida que los usuarios navegan por tu app

Cómo usar el FragmentManager

El FragmentManager administra la pila de actividades del fragmento. En el tiempo de ejecución, FragmentManager puede realizar operaciones de pila de actividades como agregar o quitar fragmentos en respuesta a interacciones del usuario. Cada conjunto de cambios se confirma como una sola unidad llamada FragmentTransaction. Para obtener un análisis más detallado sobre las transacciones de fragmentos, consulta la guía de transacciones de fragmentos.

Cuando el usuario presiona el botón Atrás en su dispositivo o llamas a FragmentManager.popBackStack(), la transacción del fragmento que se encuentra en la parte superior de la pila se quita de ella. En otras palabras, se revierte la transacción. Si no hay más transacciones de fragmentos en la pila y no estás usando fragmentos secundarios, el evento Atrás aparecerá en la actividad. Si estás usando fragmentos secundarios, consulta las consideraciones especiales para fragmentos secundarios y del mismo nivel.

Cuando llamas a addToBackStack() en una transacción, ten en cuenta que esta última puede incluir cualquier cantidad de operaciones, como agregar varios fragmentos y reemplazar fragmentos en múltiples contenedores, entre otras. Cuando se abre la pila de actividades, todas estas operaciones se revierten como una acción atómica única. Si confirmaste transacciones adicionales antes de la llamada a popBackStack() y no usaste addToBackStack() para la transacción, no se revertirán esas operaciones. Por lo tanto, dentro de una única FragmentTransaction, evita intercalar transacciones que afectan la pila de actividades con aquellas que no lo hacen.

Cómo realizar una transacción

Para mostrar un fragmento dentro de un contenedor de diseños, usa el FragmentManager a fin de crear una FragmentTransaction. Dentro de la transacción, puedes realizar una operación add() o replace() en el contenedor.

Por ejemplo, una FragmentTransaction simple puede verse de la siguiente manera:

Kotlin

supportFragmentManager.commit {
   replace<ExampleFragment>(R.id.fragment_container)
   setReorderingAllowed(true)
   addToBackStack("name") // name can be null
}

Java

FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
    .replace(R.id.fragment_container, ExampleFragment.class, null)
    .setReorderingAllowed(true)
    .addToBackStack("name") // name can be null
    .commit();

En este ejemplo, ExampleFragment reemplaza al fragmento, si hay alguno, que se encuentra actualmente en el contenedor de diseño identificado por el ID de R.id.fragment_container. Proporcionar la clase del fragmento al método replace() permite que FragmentManager controle la creación de una instancia mediante su FragmentFactory. Para obtener más información, consulta Cómo proporcionar dependencias.

setReorderingAllowed(true) optimiza los cambios de estado de los fragmentos involucrados en la transacción de modo que las animaciones y transiciones funcionen correctamente. Para obtener más información sobre la navegación con animaciones y transiciones, consulta Transacciones de fragmentos y Cómo navegar entre fragmentos con animaciones.

Llamar a addToBackStack() confirma la transacción a la pila de actividades. Luego, el usuario puede revertir la transacción y recuperar el fragmento anterior presionando el botón Atrás. Si agregaste o quitaste varios fragmentos dentro de una sola transacción, se descartarán todas las operaciones cuando se abra la pila de actividades. El nombre opcional proporcionado en la llamada addToBackStack() te permite volver a esa transacción específica con popBackStack().

Si no llamas a addToBackStack() cuando realizas una transacción que quita un fragmento, cuando se confirme la transacción, se destruirá el fragmento que se quitó, y el usuario no podrá volver a él. Si llamas a addToBackStack() cuando quitas un fragmento, ese fragmento pasa al estado STOPPED y, luego, a RESUMED cuando el usuario vuelva a él. Ten en cuenta que su vista se destruye en este caso. Para obtener más información, consulta Ciclo de vida de los fragmentos.

Cómo buscar un fragmento existente

Puedes obtener una referencia al fragmento actual dentro de un contenedor de diseños por medio de findFragmentById(). Usa findFragmentById() para buscar un fragmento por el ID dado cuando se lo aumenta desde XML o bien por el ID del contenedor cuando se lo agrega a una FragmentTransaction. Por ejemplo:

Kotlin

supportFragmentManager.commit {
   replace<ExampleFragment>(R.id.fragment_container)
   setReorderingAllowed(true)
   addToBackStack(null)
}

...

val fragment: ExampleFragment =
        supportFragmentManager.findFragmentById(R.id.fragment_container) as ExampleFragment

Java

FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
    .replace(R.id.fragment_container, ExampleFragment.class, null)
    .setReorderingAllowed(true)
    .addToBackStack(null)
    .commit();

...

ExampleFragment fragment =
        (ExampleFragment) fragmentManager.findFragmentById(R.id.fragment_container);

Como alternativa, puedes asignar una etiqueta única a un fragmento y obtener una referencia con findFragmentByTag(). Puedes asignar una etiqueta con el atributo XML android:tag en los fragmentos definidos de tu diseño o durante una operación add() o replace() dentro de una FragmentTransaction.

Kotlin

supportFragmentManager.commit {
   replace<ExampleFragment>(R.id.fragment_container, "tag")
   setReorderingAllowed(true)
   addToBackStack(null)
}

...

val fragment: ExampleFragment =
        supportFragmentManager.findFragmentByTag("tag") as ExampleFragment

Java

FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
    .replace(R.id.fragment_container, ExampleFragment.class, null, "tag")
    .setReorderingAllowed(true)
    .addToBackStack(null)
    .commit();

...

ExampleFragment fragment = (ExampleFragment) fragmentManager.findFragmentByTag("tag");

Consideraciones especiales para fragmentos secundarios y del mismo nivel

Solo se permite un FragmentManager para controlar la pila de actividades del fragmento en todo momento. Si tu app muestra varios fragmentos del mismo nivel en la pantalla al mismo tiempo o si usa fragmentos secundarios, se debe designar un FragmentManager para controlar la navegación principal de tu app.

A fin de definir el fragmento de navegación principal dentro de una transacción de fragmento, llama al método setPrimaryNavigationFragment() en la transacción y pasa la instancia del fragmento cuyo childFragmentManager debe tener el control principal.

Considera la estructura de navegación como una serie de capas, en la que la actividad es la capa más externa, que encierra cada capa de fragmentos secundarios debajo. Cada capa debe tener un solo fragmento de navegación principal. Cuando ocurre el evento Atrás, la capa más interna controla el comportamiento de navegación. Cuando esa capa no tenga más transacciones de fragmentos para mostrar, el control regresará a la siguiente capa, y este proceso se repetirá hasta llegar a la actividad.

Ten en cuenta que cuando se muestran dos o más fragmentos al mismo tiempo, solo uno de ellos puede ser el fragmento de navegación principal. Configurar un fragmento como el de navegación principal quita la designación del fragmento anterior. Como se muestra en el ejemplo anterior, si estableces el fragmento de detalle como el fragmento de navegación principal, se quitará la designación del fragmento principal.

Cómo brindar compatibilidad con varias pilas de actividades

En algunos casos, es posible que tu app necesite admitir varias pilas de actividades. Un ejemplo común es si tu app usa una barra de navegación inferior. FragmentManager te permite admitir varias pilas de actividades con los métodos saveBackStack() y restoreBackStack(), que te permiten intercambiar pilas de actividades guardando una pila y restableciendo una diferente.

saveBackStack() funciona de manera similar a llamar a popBackStack() con el parámetro opcional name: se resaltan las transacciones especificadas y todas las transacciones posteriores en la pila. La diferencia es que saveBackStack() guarda el estado de todos los fragmentos en las transacciones emergentes.

Por ejemplo, supongamos que agregaste un fragmento a la pila de actividades mediante la confirmación de una FragmentTransaction con addToBackStack():

Kotlin

supportFragmentManager.commit {
  replace<ExampleFragment>(R.id.fragment_container)
  setReorderingAllowed(true)
  addToBackStack("replacement")
}

Java

supportFragmentManager.beginTransaction()
  .replace(R.id.fragment_container, ExampleFragment.class, null)
  // setReorderingAllowed(true) and the optional string argument for
  // addToBackStack() are both required if you want to use saveBackStack().
  .setReorderingAllowed(true)
  .addToBackStack("replacement")
  .commit();

En ese caso, puedes guardar esta transacción de fragmento y el estado de ExampleFragment llamando a saveState():

Kotlin

supportFragmentManager.saveBackStack("replacement")

Java

supportFragmentManager.saveBackStack("replacement");

Puedes llamar a restoreBackStack() con el mismo parámetro de nombre para restablecer todas las transacciones emergentes y todos los estados del fragmento guardado:

Kotlin

supportFragmentManager.restoreBackStack("replacement")

Java

supportFragmentManager.restoreBackStack("replacement");

Cómo proporcionar dependencias a tus fragmentos

Cuando agregas un fragmento, puedes crear una instancia del fragmento en forma manual y agregarla a la FragmentTransaction.

Kotlin

fragmentManager.commit {
    // Instantiate a new instance before adding
    val myFragment = ExampleFragment()
    add(R.id.fragment_view_container, myFragment)
    setReorderingAllowed(true)
}

Java

// Instantiate a new instance before adding
ExampleFragment myFragment = new ExampleFragment();
fragmentManager.beginTransaction()
    .add(R.id.fragment_view_container, myFragment)
    .setReorderingAllowed(true)
    .commit();

Cuando confirmas la transacción del fragmento, la instancia creada del fragmento es la instancia que se usará. No obstante, durante un cambio de configuración, tu actividad y todos sus fragmentos se destruyen y se vuelven a crear mediante los recursos de Android más aplicables. El FragmentManager se encarga de todo esto por ti. Recrea instancias de tus fragmentos, las adjunta al host y vuelve a crear el estado de la pila de actividades.

De forma predeterminada, el FragmentManager usa una FragmentFactory que el framework proporciona para crear una instancia nueva de tu fragmento. Esta fábrica predeterminada usa el reflejo para encontrar e invocar un constructor sin argumentos para el fragmento. Eso significa que no puedes usarla a fin de proporcionar dependencias a tu fragmento. También significa que cualquier constructor personalizado que hayas usado para crear el fragmento la primera vez no se usará durante la recreación de forma predeterminada.

Para proporcionar dependencias a tu fragmento o usar cualquier constructor personalizado, debes crear una subclase de FragmentFactory personalizada y anular FragmentFactory.instantiate. Luego, puedes anular la fábrica predeterminada del FragmentManager con la tuya personalizada, que se usará para crear las instancias de los fragmentos.

Supongamos que tienes un DessertsFragment que se encarga de mostrar postres populares en tu ciudad natal. DessertsFragment tiene una dependencia en una clase DessertsRepository que le proporciona la información que necesita para mostrar la IU correcta al usuario.

Podrías definir tu DessertsFragment de forma que requiera una instancia de DessertsRepository en su constructor.

Kotlin

class DessertsFragment(val dessertsRepository: DessertsRepository) : Fragment() {
    ...
}

Java

public class DessertsFragment extends Fragment {
    private DessertsRepository dessertsRepository;

    public DessertsFragment(DessertsRepository dessertsRepository) {
        super();
        this.dessertsRepository = dessertsRepository;
    }

    // Getter omitted.

    ...
}

Una implementación simple de tu FragmentFactory podría ser similar a la siguiente.

Kotlin

class MyFragmentFactory(val repository: DessertsRepository) : FragmentFactory() {
    override fun instantiate(classLoader: ClassLoader, className: String): Fragment =
            when (loadFragmentClass(classLoader, className)) {
                DessertsFragment::class.java -> DessertsFragment(repository)
                else -> super.instantiate(classLoader, className)
            }
}

Java

public class MyFragmentFactory extends FragmentFactory {
    private DessertsRepository repository;

    public MyFragmentFactory(DessertsRepository repository) {
        super();
        this.repository = repository;
    }

    @NonNull
    @Override
    public Fragment instantiate(@NonNull ClassLoader classLoader, @NonNull String className) {
        Class<? extends Fragment> fragmentClass = loadFragmentClass(classLoader, className);
        if (fragmentClass == DessertsFragment.class) {
            return new DessertsFragment(repository);
        } else {
            return super.instantiate(classLoader, className);
        }
    }
}

En este ejemplo, se crean subclases de FragmentFactory, lo que anula el método instantiate() a fin de proporcionar una lógica de creación de fragmentos personalizada para un DessertsFragment. El comportamiento predeterminado de FragmentFactory controla mediante super.instantiate() otras clases de fragmentos.

Luego, puedes designar MyFragmentFactory como la fábrica que se usará cuando construyas los fragmentos de tu app configurando una propiedad en el FragmentManager. Debes establecer esta propiedad antes del objeto super.onCreate() de tu actividad para asegurarte de que se usará MyFragmentFactory cuando vuelvas a crear los fragmentos.

Kotlin

class MealActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        supportFragmentManager.fragmentFactory = MyFragmentFactory(DessertsRepository.getInstance())
        super.onCreate(savedInstanceState)
    }
}

Java

public class MealActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        DessertsRepository repository = DessertsRepository.getInstance();
        getSupportFragmentManager().setFragmentFactory(new MyFragmentFactory(repository));
        super.onCreate(savedInstanceState);
    }
}

Ten en cuenta que configurar la FragmentFactory en la actividad anula la creación de fragmentos en toda la jerarquía de fragmentos de la actividad. En otras palabras, el childFragmentManager de cualquier fragmento secundario que agregues usa la fábrica de fragmentos personalizada, a menos que se la anule en un nivel inferior.

Pruebas con FragmentFactory

En una arquitectura de actividad única, debes probar tus fragmentos de forma aislada usando la clase FragmentScenario. Dado que no puedes basarte en la lógica de onCreate personalizada de tu actividad, puedes pasar la FragmentFactory como un argumento en la prueba de fragmentos, como se muestra en el siguiente ejemplo:

// Inside your test
val dessertRepository = mock(DessertsRepository::class.java)
launchFragment<DessertsFragment>(factory = MyFragmentFactory(dessertRepository)).onFragment {
    // Test Fragment logic
}

Para obtener información detallada sobre este proceso de prueba y ver ejemplos completos, consulta Cómo probar los fragmentos de tu app.