Gerenciador de fragmentos

.

FragmentManager é a classe responsável por executar ações nos fragmentos do app, como adicionar, remover ou substituir, e adicioná-los à pilha de retorno.

Talvez você nunca interaja com o FragmentManager diretamente se estiver usando a biblioteca Jetpack Navigation, já que ela trabalha com FragmentManager em seu nome. Sendo assim, qualquer app que use fragmentos está usando o FragmentManager em algum nível, então é importante entender o que ele é e como funciona.

Este tópico aborda como acessar o FragmentManager, o papel do FragmentManager em relação às suas atividades e aos seus fragmentos, como gerenciar a pilha de retorno com FragmentManager e como fornecer dados e dependências aos seus fragmentos.

Acessar o FragmentManager

Como acessar em uma atividade

Cada FragmentActivity e subclasse, como a AppCompatActivity, tem acesso ao FragmentManager pelo método getSupportFragmentManager().

Como acessar em um fragmento

Os fragmentos também são capazes de hospedar um ou mais fragmentos filhos. Dentro de um fragmento, é possível conseguir uma referência ao FragmentManager que gerencia os filhos do fragmento pelo getChildFragmentManager(). Se você precisar acessar o host FragmentManager, use getParentFragmentManager().

Veja alguns exemplos para conhecer as relações entre os fragmentos, os hosts deles e as instâncias do FragmentManager associadas a cada um.

Dois exemplos de layout da IU que mostram as relações entre
            os fragmentos e as atividades de host deles.
Figura 1. Dois exemplos de layout da IU que mostram as relações entre fragmentos e as atividades de host deles.

A Figura 1 mostra dois exemplos, cada um deles com um único host de atividade. A atividade do host em ambos os exemplos exibe uma navegação de nível superior para o usuário como uma BottomNavigationView responsável por trocar o fragmento de host com diferentes telas no app, com cada uma implementada como um fragmento separado.

O fragmento de host no Exemplo 1 hospeda dois fragmentos filhos que compõem uma tela de visualização dividida. O fragmento de host no Exemplo 2 hospeda um único fragmento filho que compõe o fragmento de exibição de uma visualização deslizável.

Considerando essa configuração, é possível pensar em cada host como tendo um FragmentManager associado a ele e que gerencia os fragmentos filhos. Isso é ilustrado na Figura 2, assim como os mapeamentos de propriedades entre supportFragmentManager, parentFragmentManager e childFragmentManager.

Cada host tem o próprio FragmentManager associado
            que gerencia os fragmentos filhos
Figura 2. Cada host tem o próprio FragmentManager associado que gerencia os fragmentos filhos.

A propriedade FragmentManager adequada para referência depende de onde está o local de chamadas na hierarquia de fragmentos e de qual gerenciador de fragmentos você está tentando usar para o acesso.

Quando você tiver uma referência ao FragmentManager, poderá usá-la para manipular os fragmentos exibidos ao usuário.

Fragmentos filhos

De modo geral, o app precisa consistir em um número pequeno ou único de atividades no projeto do aplicativo, com cada uma representando um grupo de telas relacionadas. A atividade pode fornecer um ponto para posicionar a navegação de nível superior e um local para o escopo de ViewModels e outro estado de visualização entre fragmentos. Cada destino individual no app precisa ser representado por um fragmento.

Se você quiser exibir vários fragmentos de uma só vez, como em uma visualização dividida ou em um painel, use fragmentos filhos gerenciados pelo fragmento de destino e pelo gerenciador de fragmentos filhos.

Outros casos de uso para fragmentos filhos podem incluir os seguintes:

  • Deslizes de tela, com um ViewPager2 em um fragmento pai para gerenciar uma série de visualizações do fragmento filho.
  • Subnavegação em um conjunto de telas relacionadas.
  • O Jetpack Navigation usa fragmentos filhos como destinos individuais. Uma atividade hospeda um único NavHostFragment pai e preenche o espaço com diferentes fragmentos de destino filhos à medida que os usuários navegam pelo app.

Como usar o FragmentManager

O FragmentManager gerencia a pilha de retorno de fragmentos. No ambiente de execução, o FragmentManager pode executar operações de pilha de retorno, como a adição ou remoção de fragmentos em resposta a interações do usuário. Cada conjunto de mudanças é combinado como uma única unidade chamada FragmentTransaction. Para ver uma discussão mais aprofundada sobre transações de fragmentos, consulte o guia de transações de fragmentos.

Quando o usuário pressiona o botão Voltar no dispositivo ou quando você chama FragmentManager.popBackStack(), a transação de fragmento na camada superior é removida da pilha. Em outras palavras, a transação é revertida. Se não houver mais transações de fragmentos na pilha e se você não estiver usando fragmentos filhos, o evento de retorno surgirá na atividade. Se você estiver usando fragmentos filhos, consulte as considerações especiais para fragmentos filhos e irmãos.

Quando você chama addToBackStack() em uma transação, ela pode incluir qualquer número de operações, como a adição de vários fragmentos, substituição de fragmentos em vários contêineres e assim por diante. Quando a pilha de retorno é encerrada, todas essas operações são revertidas para uma única ação atômica. Se você tiver confirmado outras transações antes da chamada de popBackStack() e se não tiver usado addToBackStack() na transação, essas operações não serão revertidas. Portanto, em uma única FragmentTransaction, evite intercalar transações que afetam a pilha de retorno com as que não fazem isso.

Realizar uma transação

Para exibir um fragmento em um contêiner de layout, use o FragmentManager para criar uma FragmentTransaction. Dentro da transação, é possível realizar uma operação add() ou replace() no contêiner.

Por exemplo, uma FragmentTransaction simples pode ter esta aparência:

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

Nesse exemplo, ExampleFragment substitui o fragmento, se houver, que está no contêiner de layout identificado pelo ID R.id.fragment_container. Fornecer a classe do fragmento ao método replace() permite que o FragmentManager processe a instanciação usando FragmentFactory. Para mais informações, consulte Como fornecer dependências.

setReorderingAllowed(true) otimiza as mudanças de estado dos fragmentos envolvidos na transação para que as animações e transições funcionem corretamente. Para ver mais informações sobre como navegar com animações e transições, consulte Transações de fragmentos e Navegar entre fragmentos usando animações.

Chamar addToBackStack() confirma a transação na pilha de retorno. Posteriormente, o usuário pode reverter a transação e recuperar o fragmento anterior pressionando o botão Voltar. Se você adicionou ou removeu vários fragmentos em uma única transação, todas essas operações serão desfeitas quando a pilha de retorno for retirada. O nome opcional fornecido na chamada addToBackStack() permite retornar para essa transação específica usando popBackStack().

Se você não chamar addToBackStack() ao realizar uma transação que remove um fragmento, ele será destruído quando a transação for confirmada, e o usuário não poderá navegar de volta a ele. Se você chamar addToBackStack() ao remover um fragmento, ele será apenas STOPPED e, mais tarde, será RESUMED quando o usuário navegar de volta. Observe que a visualização está destruída nesse caso. Para ver mais informações, consulte Ciclo de vida do fragmento.

Como encontrar um fragmento existente

Você pode conseguir uma referência ao fragmento atual em um contêiner de layout usando findFragmentById(). Use findFragmentById() para procurar um fragmento pelo ID fornecido quando inflado a partir do XML ou pelo ID do contêiner quando adicionado em uma FragmentTransaction. Veja um exemplo:

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, você pode atribuir uma tag exclusiva a um fragmento e conseguir uma referência usando findFragmentByTag(). É possível atribuir uma tag usando o atributo XML android:tag em fragmentos que são definidos no seu layout ou durante uma operação de add() ou replace() em uma 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");

Considerações especiais para fragmentos filhos e irmãos

Apenas um FragmentManager tem permissão para controlar a pilha de retorno do fragmento a qualquer momento. Se o app mostrar vários fragmentos irmãos na tela ao mesmo tempo ou se ele usar fragmentos filhos, um FragmentManager precisará ser designado para processar a navegação principal do app.

Para definir o fragmento de navegação principal em uma transação de fragmento, chame o método setPrimaryNavigationFragment() na transação, transmitindo na instância do fragmento cujo childFragmentManager precisa ter o controle principal.

Pense na estrutura de navegação como uma série de camadas, com a atividade como a camada mais externa, envolvendo cada camada de fragmentos filhos dentro dela. Cada camada precisa ter um único fragmento de navegação principal. Quando o evento de retorno ocorre, a camada mais interna controla o comportamento de navegação. Quando a camada mais interna não tem mais transações de fragmentos de onde retornar, o controle retorna à próxima camada externa, e esse processo é repetido até você chegar à atividade.

Quando dois ou mais fragmentos são exibidos ao mesmo tempo, apenas um deles pode ser o fragmento de navegação principal. Definir um fragmento como o de navegação principal remove a designação do fragmento anterior. Usando o exemplo anterior, se você definir o fragmento de detalhe como o de navegação principal, a designação do fragmento principal será removida.

Compatibilidade com várias backstacks

Em alguns casos, seu app pode precisar ser compatível com várias backstacks. Um exemplo comum é quando o app usa uma barra de navegação inferior. FragmentManager permite que você seja compatível com várias backstacks com os métodos saveBackStack() e restoreBackStack(). Esses métodos permitem alternar entre as backstacks salvando uma pilha de retorno e restaurando outra diferente.

saveBackStack() funciona de maneira semelhante a chamar popBackStack() com o parâmetro opcional name: a transação especificada e todas as transações depois dela na pilha são exibidas. A diferença é que saveBackStack() salva o estado de todos os fragmentos nas transações exibidas.

Por exemplo, suponha que você já tenha adicionado um fragmento à backstack confirmando um FragmentTransaction usando 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();

Nesse caso, é possível salvar essa transação de fragmento e o estado de ExampleFragment chamando saveState():

Kotlin

supportFragmentManager.saveBackStack("replacement")

Java

supportFragmentManager.saveBackStack("replacement");

É possível chamar restoreBackStack() com o mesmo parâmetro de nome para restaurar todas as transações exibidas e todos os estados do fragmento salvos:

Kotlin

supportFragmentManager.restoreBackStack("replacement")

Java

supportFragmentManager.restoreBackStack("replacement");

Fornecer dependências para seus fragmentos

Ao adicionar um fragmento, é possível instanciá-lo manualmente e adicioná-lo à 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 você confirma a transação do fragmento, a instância do fragmento criado é a usada. No entanto, durante uma mudança de configuração, sua atividade e todos os fragmentos dela são destruídos e, em seguida, recriados com os recursos Android mais aplicáveis. O FragmentManager processa tudo isso para você. Ele recria instâncias dos seus fragmentos, anexa-as ao host e recria o estado da pilha de retorno.

Por padrão, o FragmentManager usa um FragmentFactory fornecido pelo framework para instanciar uma nova instância do seu fragmento. Essa fábrica padrão usa reflexão para localizar e invocar um construtor sem argumento para seu fragmento. Isso significa que não é possível usar essa fábrica padrão para fornecer dependências para seu fragmento. Além disso, por padrão, qualquer construtor personalizado usado para criar o fragmento na primeira vez não será usado durante a recriação.

Para fornecer dependências ao seu fragmento ou usar qualquer construtor personalizado, crie uma subclasse FragmentFactory personalizada e, em seguida, substitua FragmentFactory.instantiate. Depois disso, você pode substituir a fábrica padrão do FragmentManager pelo factory personalizada, que será usado para instanciar seus fragmentos.

Imagine que você tem um DessertsFragment responsável por exibir sobremesas famosas na sua cidade natal. Vamos supor que DessertsFragment tem uma dependência em uma classe DessertsRepository que fornece as informações necessárias para exibir a IU correta para o usuário.

Você pode definir seu DessertsFragment para exigir uma instância de DessertsRepository no construtor.

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.

    ...
}

Uma implementação simples do seu FragmentFactory pode ficar semelhante a esta:

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

Esse exemplo de subclasses FragmentFactory substitui o método instantiate() para fornecer uma lógica personalizada de criação de fragmentos para um DessertsFragment. Outras classes de fragmentos são processadas pelo comportamento padrão de FragmentFactory a super.instantiate().

Você pode designar MyFragmentFactory como a fábrica a ser usada ao construir os fragmentos do app definindo uma propriedade no FragmentManager. Defina essa propriedade antes do super.onCreate() da atividade para garantir que MyFragmentFactory seja usado ao recriar seus 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);
    }
}

Observe que a definição de FragmentFactory na atividade substitui a criação de fragmentos em toda a hierarquia de fragmentos da atividade. Em outras palavras, o childFragmentManager de todos os fragmentos filhos que você adiciona usa o conjunto de fábrica de fragmentos personalizado mostrado aqui, a menos que seja substituído em um nível inferior.

Como testar com FragmentFactory

Em uma única arquitetura de atividade, teste os fragmentos em isolamento usando a classe FragmentScenario. Como não é possível confiar na lógica onCreate personalizada da sua atividade, você pode transmitir o FragmentFactory como um argumento para o teste de fragmentos, conforme mostrado no exemplo a seguir:

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

Para ver informações detalhadas sobre esse processo de testagem e exemplos completos, consulte Testar os fragmentos do seu app.