Gestore di frammenti

FragmentManager è la classe responsabile dell'esecuzione di azioni sui frammenti dell'app, ad esempio l'aggiunta, la rimozione o la sostituzione e l'aggiunta allo stack di back-forward.

Non puoi mai interagire direttamente con FragmentManager se utilizzi la libreria Jetpack Navigation, che funziona con FragmentManager per tuo conto. Tuttavia, qualsiasi app che utilizza i frammenti utilizza FragmentManager a un certo livello, quindi è importante capire di cosa si tratta e come funziona.

In questa pagina vengono trattati i seguenti argomenti:

  • Come accedere al FragmentManager.
  • Il ruolo di FragmentManager in relazione alle tue attività e ai tuoi frammenti.
  • Come gestire lo stack precedente con FragmentManager.
  • Come fornire dati e dipendenze ai frammenti.

Accedere a FragmentManager

Puoi accedere a FragmentManager da un'attività o da un frammento.

FragmentActivity e le sue sottoclassi, come AppCompatActivity, hanno accesso alla FragmentManager tramite il metodo getSupportFragmentManager().

I frammenti possono ospitare uno o più frammenti figlio. All'interno di un frammento, puoi ottenere un riferimento al FragmentManager che gestisce gli elementi secondari del frammento tramite getChildFragmentManager(). Se devi accedere al relativo host FragmentManager, puoi utilizzare getParentFragmentManager().

Ecco un paio di esempi per vedere le relazioni tra i frammenti, i relativi host e le istanze FragmentManager associate a ognuno.

due esempi di layout dell'interfaccia utente che mostrano le relazioni tra i frammenti e le relative attività host
Figura 1. Due esempi di layout dell'interfaccia utente che mostrano le relazioni tra i frammenti e le relative attività host.

La Figura 1 mostra due esempi, ognuno dei quali ha un singolo host attività. L'attività host in entrambi questi esempi mostra all'utente la navigazione di primo livello come BottomNavigationView responsabile della sostituzione del frammento host con schermate diverse nell'app. Ogni schermata viene implementata come un frammento separato.

Il frammento host nell'esempio 1 ospita due frammenti figlio che compongono uno schermo divisa. Il frammento host nell'esempio 2 ospita un singolo frammento figlio che costituisce il frammento di visualizzazione di una visualizzazione a scorrimento.

Data questa configurazione, puoi considerare ogni host come un elemento FragmentManager associato che gestisce i relativi frammenti figlio. Questo è illustrato nella Figura 2 insieme alle mappature delle proprietà tra supportFragmentManager, parentFragmentManager e childFragmentManager.

a ogni host è associato il proprio FragmentManager che gestisce i relativi frammenti figlio
Figura 2. A ogni host è associato il proprio FragmentManager che gestisce i frammenti figlio.

La proprietà FragmentManager appropriata a cui fare riferimento dipende da dove si trova il sito di chiamata nella gerarchia dei frammenti e dal gestore di frammenti a cui stai tentando di accedere.

Una volta ottenuto un riferimento a FragmentManager, puoi utilizzarlo per manipolare i frammenti visualizzati dall'utente.

Frammenti figlio

In generale, l'app è composta da un numero singolo o ridotto di attività nel progetto dell'applicazione e ogni attività rappresenta un gruppo di schermate correlate. L'attività potrebbe fornire un punto in cui posizionare la navigazione di primo livello e un punto in cui definire l'ambito degli oggetti ViewModel e di altri stati di visualizzazione tra i frammenti. Un frammento rappresenta una singola destinazione nella tua app.

Se vuoi mostrare più frammenti contemporaneamente, ad esempio in una visualizzazione divisa o in una dashboard, puoi utilizzare i frammenti figlio gestiti dal frammento di destinazione e dal relativo gestore di frammenti figlio.

Altri casi d'uso per i frammenti figlio sono i seguenti:

  • Schermata di diapositive utilizzando un elemento ViewPager2 in un frammento principale per gestire una serie di visualizzazioni di frammenti figlio.
  • Navigazione secondaria all'interno di una serie di schermate correlate.
  • Jetpack Navigation utilizza i frammenti secondari come singole destinazioni. Un'attività ospita un unico elemento NavHostFragment principale e riempie il suo spazio con diversi frammenti di destinazione figlio mentre gli utenti navigano nell'app.

Utilizzare FragmentManager

FragmentManager gestisce lo stack di back dei frammenti. In fase di runtime, FragmentManager può eseguire operazioni di back stack come l'aggiunta o la rimozione di frammenti in risposta alle interazioni degli utenti. Ogni insieme di modifiche viene associato come singola unità denominata FragmentTransaction. Per una discussione più approfondita sulle transazioni con frammenti, consulta la guida alle transazioni con frammenti.

Quando l'utente tocca il pulsante Indietro sul dispositivo o quando chiami FragmentManager.popBackStack(), dallo stack esce la transazione del frammento più in alto. Se non ci sono più transazioni sui frammenti nello stack e se non stai utilizzando i frammenti figlio, l'evento Indietro mostra all'attività. Se utilizzi frammenti figlio, consulta considerazioni particolari per i frammenti figlio e di pari livello.

Quando chiami addToBackStack() in una transazione, quest'ultima può includere un numero illimitato di operazioni, come l'aggiunta di più frammenti o la sostituzione di frammenti in più container.

Quando si apre lo stack posteriore, tutte queste operazioni vengono invertite come una singola azione atomica. Tuttavia, se hai eseguito il commit di ulteriori transazioni prima della chiamata popBackStack() e se non hai utilizzato addToBackStack() per la transazione, queste operazioni non vengono annullate. Di conseguenza, all'interno di un singolo FragmentTransaction, evita di interpretare le transazioni che influiscono sullo stack di back-end con quelle che non lo fanno.

Eseguire una transazione

Per visualizzare un frammento all'interno di un contenitore di layout, utilizza FragmentManager per creare un elemento FragmentTransaction. All'interno della transazione, puoi quindi eseguire un'operazione add() o replace() sul container.

Ad esempio, un semplice FragmentTransaction potrebbe avere il seguente aspetto:

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();

In questo esempio, ExampleFragment sostituisce l'eventuale frammento che si trova attualmente nel contenitore di layout identificato dall'ID R.id.fragment_container. Fornisce la classe del frammento al metodo replace() consente a FragmentManager di gestire l'istanza utilizzando il relativo FragmentFactory. Per ulteriori informazioni, consulta la sezione Fornire dipendenze ai frammenti.

setReorderingAllowed(true) ottimizza le modifiche di stato dei frammenti coinvolti nella transazione in modo che le animazioni e le transizioni funzionino correttamente. Per ulteriori informazioni sulla navigazione con le animazioni e le transizioni, consulta Transazioni relative ai frammenti e Spostati tra i frammenti utilizzando le animazioni.

La chiamata a addToBackStack() commette il commit della transazione nello stack di backup. L'utente può in seguito invertire la transazione e ripristinare il frammento precedente toccando il pulsante Indietro. Se hai aggiunto o rimosso più frammenti in una singola transazione, tutte queste operazioni vengono annullate quando viene saltato lo stack posteriore. Il nome facoltativo fornito nella chiamata addToBackStack() ti consente di tornare a una transazione specifica utilizzando popBackStack().

Se non chiami addToBackStack() quando esegui una transazione che rimuove un frammento, il frammento rimosso viene eliminato quando viene eseguito il commit della transazione e l'utente non può accedervi nuovamente. Se chiami addToBackStack() durante la rimozione di un frammento, questo sarà solo STOPPED e, in un secondo momento, sarà RESUMED quando l'utente torna indietro. In questo caso, la sua visualizzazione viene eliminata. Per ulteriori informazioni, consulta Ciclo di vita dei frammenti.

Trova un frammento esistente

Puoi ottenere un riferimento al frammento attuale all'interno di un container di layout utilizzando findFragmentById(). Utilizza findFragmentById() per cercare un frammento in base all'ID specificato quando viene aumentato in modo artificioso da XML o in base all'ID contenitore quando viene aggiunto in FragmentTransaction. Ecco un esempio:

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);

In alternativa, puoi assegnare un tag univoco a un frammento e ottenere un riferimento utilizzando findFragmentByTag(). Puoi assegnare un tag utilizzando l'attributo XML android:tag sui frammenti definiti all'interno del tuo layout o durante un'operazione add() o replace() all'interno di un'istanza 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");

Considerazioni speciali per i frammenti figlio e di pari livello

Solo un elemento FragmentManager può controllare lo stack di back stack dei frammenti alla volta. Se la tua app mostra più frammenti di pari livello sullo schermo contemporaneamente o se l'app utilizza frammenti figlio, un FragmentManager è designato per gestire la navigazione principale dell'app.

Per definire il frammento di navigazione principale all'interno di una transazione di frammento, chiama il metodo setPrimaryNavigationFragment() sulla transazione, passando l'istanza del frammento il cui childFragmentManager ha il controllo principale.

Considera la struttura di navigazione come una serie di livelli, in cui l'attività è il livello più esterno e racchiude ogni livello di frammenti figlio al di sotto. Ogni livello ha un singolo frammento di navigazione principale.

Quando si verifica l'evento Indietro, il livello più interno controlla il comportamento di navigazione. Una volta che il livello più interno non ha più transazioni con i frammenti da cui derivare, il controllo torna al livello successivo e questo processo si ripete finché non raggiungi l'attività.

Quando vengono visualizzati contemporaneamente due o più frammenti, solo uno di questi rappresenta il frammento di navigazione principale. L'impostazione di un frammento come frammento di navigazione principale rimuove la designazione dal frammento precedente. Utilizzando l'esempio precedente, se imposti il frammento di dettaglio come frammento di navigazione principale, la designazione del frammento principale viene rimossa.

Supporto di più back stack

In alcuni casi, l'app potrebbe dover supportare più back stack. Un esempio comune è se l'app utilizza una barra di navigazione in basso. FragmentManager consente di supportare più back stack con i metodi saveBackStack() e restoreBackStack(). Questi metodi consentono di passare da uno stack all'altro salvando uno stack precedente e ripristinandone uno diverso.

saveBackStack() funziona in modo simile alla chiamata di popBackStack() con il parametro facoltativo name: vengono visualizzati tramite popup la transazione specificata e tutte quelle successive nello stack. La differenza è che saveBackStack() salva lo stato di tutti i frammenti nelle transazioni visualizzate tramite POP.

Ad esempio, supponi di aver precedentemente aggiunto un frammento allo stack di supporto eseguendo il commit di un FragmentTransaction utilizzando addToBackStack(), come mostrato nell'esempio seguente:

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();

In questo caso, puoi salvare questa transazione con frammento e lo stato di ExampleFragment chiamando saveBackStack():

Kotlin

supportFragmentManager.saveBackStack("replacement")

Java

supportFragmentManager.saveBackStack("replacement");

Puoi chiamare restoreBackStack() con lo stesso parametro del nome per ripristinare tutte le transazioni popup e tutti gli stati dei frammenti salvati:

Kotlin

supportFragmentManager.restoreBackStack("replacement")

Java

supportFragmentManager.restoreBackStack("replacement");

Fornisci dipendenze ai tuoi frammenti

Quando aggiungi un frammento, puoi creare un'istanza del frammento manualmente e aggiungerlo a 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();

Quando esegui il commit della transazione del frammento, l'istanza del frammento che hai creato è quella utilizzata. Tuttavia, durante una modifica della configurazione, l'attività e tutti i suoi frammenti vengono eliminati e poi ricreati con le risorse Android più applicabili. L'elemento FragmentManager gestisce tutto questo per te: ricrea le istanze dei tuoi frammenti, le collega all'host e ricrea lo stato dello stack di back.

Per impostazione predefinita, FragmentManager utilizza un elemento FragmentFactory fornito dal framework per creare l'istanza di una nuova istanza del frammento. Questa fabbrica predefinita utilizza il riflesso per trovare e richiamare un costruttore senza argomento per il frammento. Ciò significa che non puoi utilizzare questo valore di fabbrica predefinito per fornire dipendenze al frammento. Ciò significa inoltre che per impostazione predefinita qualsiasi costruttore personalizzato utilizzato per creare il frammento la prima volta non viene utilizzato durante la ricreazione.

Per fornire dipendenze al frammento o per utilizzare un costruttore personalizzato, crea una sottoclasse FragmentFactory personalizzata e poi esegui l'override di FragmentFactory.instantiate. Puoi quindi eseguire l'override della fabbrica predefinita di FragmentManager con la tua fabbrica personalizzata, che viene quindi utilizzata per creare un'istanza dei frammenti.

Supponiamo che tu abbia un DessertsFragment responsabile della visualizzazione di dolci più popolari nella tua città e che DessertsFragment abbia una dipendenza da una classe DessertsRepository che gli fornisce le informazioni necessarie per mostrare l'interfaccia utente corretta all'utente.

Puoi definire DessertsFragment in modo da richiedere un'istanza DessertsRepository nel relativo costruttore.

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 semplice implementazione di FragmentFactory potrebbe essere simile alla seguente.

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);
        }
    }
}

Questo esempio esegue le sottoclassi FragmentFactory, sostituendo il metodo instantiate() per fornire una logica di creazione personalizzata dei frammenti per un oggetto DessertsFragment. Altre classi di frammenti vengono gestite dal comportamento predefinito di FragmentFactory tramite super.instantiate().

Puoi quindi designare MyFragmentFactory come fabbrica da utilizzare durante la creazione dei frammenti della tua app impostando una proprietà in FragmentManager. Devi impostare questa proprietà prima di super.onCreate() dell'attività per assicurarti che MyFragmentFactory venga utilizzato durante la ricreazione dei frammenti.

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);
    }
}

L'impostazione di FragmentFactory nell'attività sostituisce la creazione di frammenti in tutta la gerarchia di frammenti dell'attività. In altre parole, l'elemento childFragmentManager di eventuali frammenti figlio che aggiungi utilizza il valore di fabbrica dei frammenti personalizzati impostato qui, a meno che non venga eseguito l'override a un livello inferiore.

Test con FragmentFactory

In un'architettura a singola attività, testa i frammenti isolandoli utilizzando la classe FragmentScenario. Poiché non puoi fare affidamento sulla logica onCreate personalizzata delle tue attività, puoi passare FragmentFactory come argomento al test dei frammenti, come mostrato nell'esempio seguente:

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

Per informazioni dettagliate su questo processo di test ed esempi completi, consulta Test dei frammenti.