Менеджер фрагментов

FragmentManager – это класс, отвечающий за выполнение действий с фрагментами приложения, таких как добавление, удаление или замена, а также добавление в обратный стек.

Если вы используете библиотеку Jetpack Navigation, то можете никогда не взаимодействовать с FragmentManager напрямую, поскольку она работает с FragmentManager от вашего имени. Однако любое приложение, использующее фрагменты, на каком-то уровне использует FragmentManager, поэтому важно понимать, что это такое и как оно работает.

На этой странице рассказывается следующее:

  • Как получить доступ к FragmentManager.
  • Роль FragmentManager в отношении ваших действий и фрагментов.
  • Как управлять обратным стеком с помощью FragmentManager.
  • Как передавать данные и зависимости фрагментам.

Как получить доступ к FragmentManager

Вы можете получить доступ к FragmentManager из действия или фрагмента.

FragmentActivity и его подклассы, например AppCompatActivity, имеют доступ к FragmentManager через метод getSupportFragmentManager().

Фрагменты могут содержать один или несколько дочерних фрагментов. Внутри фрагмента вы можете получить ссылку на FragmentManager, который управляет дочерними элементами фрагмента, через getChildFragmentManager(). Если вам нужно получить доступ к хосту FragmentManager, используйте getParentFragmentManager().

Ниже приведены примеры, которые помогут вам понять, как связаны между собой фрагменты, их хосты и экземпляры FragmentManager.

Два примера макета пользовательского интерфейса, показывающие взаимосвязь между фрагментами и их хост-активити.
Рисунок 1. Два примера макета пользовательского интерфейса, показывающие взаимосвязи между фрагментами и их хост-активити.

На рисунке 1 показаны два примера, в каждом из которых есть один хост действий. В обоих примерах в родительском действии пользователю показывается навигация верхнего уровня в виде BottomNavigationView, который отвечает за замену родительского фрагмента разными экранами в приложении. Каждый экран реализован как отдельный фрагмент.

В примере 1 родительский фрагмент содержит два дочерних фрагмента, которые составляют разделенный экран. В примере 2 хост-фрагмент содержит один дочерний фрагмент, который составляет фрагмент отображения скользящего представления.

При такой настройке можно считать, что с каждым хостом связан FragmentManager, который управляет его дочерними фрагментами. Это показано на рисунке 2 вместе с сопоставлением свойств между supportFragmentManager, parentFragmentManager и childFragmentManager.

У каждого хоста есть собственный FragmentManager, который управляет дочерними фрагментами.
Рисунок 2. У каждого хоста есть свой FragmentManager, который управляет дочерними фрагментами.

Выбор свойства FragmentManager зависит от того, где в иерархии фрагментов находится место вызова, и от того, к какому менеджеру фрагментов вы пытаетесь получить доступ.

Получив ссылку на FragmentManager, вы можете использовать ее для управления фрагментами, которые показываются пользователю.

Дочерние фрагменты

Как правило, приложение состоит из одного или нескольких действий, каждое из которых представляет собой группу связанных экранов. В действии можно разместить навигацию верхнего уровня, а также область действия объектов ViewModel и других состояний просмотра между фрагментами. Фрагмент представляет собой отдельный пункт назначения в приложении.

Если вы хотите показать несколько фрагментов одновременно, например в режиме разделенного экрана или на панели управления, вы можете использовать дочерние фрагменты, управляемые целевым фрагментом и его менеджером дочерних фрагментов.

Другие примеры использования дочерних фрагментов:

  • Слайды экрана, в которых используется ViewPager2 в родительском фрагменте для управления серией дочерних представлений фрагментов.
  • Дополнительная навигация в наборе связанных экранов.
  • Jetpack Navigation использует дочерние фрагменты как отдельные пункты назначения. Действие содержит один родительский элемент NavHostFragment и заполняет его пространство различными дочерними фрагментами назначения по мере того, как пользователи перемещаются по приложению.

Используйте FragmentManager

FragmentManager управляет обратным стеком фрагментов. Во время выполнения FragmentManager может выполнять операции с обратным стеком, например добавлять или удалять фрагменты в ответ на действия пользователя. Каждый набор изменений фиксируется вместе как единое целое, называемое FragmentTransaction. Подробную информацию о транзакциях для работы с фрагментами можно найти в руководстве по транзакциям для работы с фрагментами.

Когда пользователь нажимает кнопку "Назад" на устройстве или когда вы вызываете FragmentManager.popBackStack(), верхняя транзакция фрагмента извлекается из стека. Если в стеке больше нет транзакций фрагментов и вы не используете дочерние фрагменты, событие Back передается в действие. Если вы используете дочерние фрагменты, ознакомьтесь с особыми рекомендациями по работе с дочерними и родственными фрагментами.

При вызове addToBackStack() для транзакции она может включать любое количество операций, например добавление нескольких фрагментов или замену фрагментов в нескольких контейнерах.

Когда обратный стек извлекается, все эти операции отменяются как одно атомарное действие. Однако если вы совершили дополнительные транзакции до вызова popBackStack() и не использовали addToBackStack() для транзакции, эти операции не отменяются. Поэтому в рамках одного FragmentTransaction не следует чередовать транзакции, которые влияют на обратный стек, с теми, которые этого не делают.

Выполнить транзакцию

Чтобы отобразить фрагмент в контейнере макета, используйте FragmentManager для создания FragmentTransaction. В рамках транзакции вы можете выполнить операцию add() или replace() с контейнером.

Например, простой FragmentTransaction может выглядеть так:

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

В этом примере ExampleFragment заменяет фрагмент, если он есть, который в данный момент находится в контейнере макета, идентифицированном идентификатором R.id.fragment_container. Если передать класс фрагмента в метод replace(), то FragmentManager сможет создать экземпляр с помощью FragmentFactory. Подробную информацию вы найдете в разделе Предоставление зависимостей фрагментам.

setReorderingAllowed(true) оптимизирует изменения состояния фрагментов, задействованных в транзакции, чтобы анимация и переходы работали правильно. Дополнительную информацию о навигации с анимацией и переходами можно найти в статьях Транзакции для работы с фрагментами и Навигация между фрагментами с помощью анимации.

Вызов addToBackStack() фиксирует транзакцию в обратном стеке. Пользователь может отменить транзакцию и вернуть предыдущий фрагмент, нажав кнопку "Назад". Если вы добавили или удалили несколько фрагментов в рамках одной транзакции, все эти операции отменяются при извлечении обратного стека. Необязательное имя, указанное в вызове addToBackStack(), позволяет вернуться к определенной транзакции с помощью popBackStack().

Если вы не вызовете addToBackStack() при выполнении транзакции, которая удаляет фрагмент, то удаленный фрагмент будет уничтожен при фиксации транзакции, и пользователь не сможет вернуться к нему. Если вы вызываете addToBackStack() при удалении фрагмента, то фрагмент только STOPPED и позже RESUMED, когда пользователь возвращается. В этом случае его представление будет удалено. Дополнительную информацию см. в статье Жизненный цикл фрагмента.

Как найти существующий фрагмент

Вы можете получить ссылку на текущий фрагмент в контейнере макета с помощью findFragmentById(). Используйте findFragmentById(), чтобы найти фрагмент по идентификатору, указанному в XML, или по идентификатору контейнера, указанному в FragmentTransaction. Рассмотрим пример.

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

Вы также можете назначить фрагменту уникальный тег и получить ссылку с помощью findFragmentByTag(). Вы можете назначить тег с помощью XML-атрибута android:tag для фрагментов, определенных в макете, или во время операции add() или replace() в 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");

Особые рекомендации для дочерних и родственных фрагментов

Только один FragmentManager может управлять обратным стеком фрагментов в любой момент времени. Если в приложении одновременно показывается несколько фрагментов одного уровня или используются дочерние фрагменты, то один из фрагментов FragmentManager назначается для обработки основной навигации.

Чтобы определить основной фрагмент навигации в транзакции фрагмента, вызовите метод setPrimaryNavigationFragment() для транзакции, передав экземпляр фрагмента, у которого childFragmentManager имеет основной контроль.

Представьте структуру навигации как серию слоев, где активность является самым внешним слоем, оборачивающим каждый слой дочерних фрагментов под ним. У каждого слоя есть один основной фрагмент навигации.

Когда происходит событие "Назад", самый внутренний слой управляет поведением навигации. Когда у самого внутреннего слоя больше не останется транзакций фрагментов, к которым можно вернуться, управление перейдет к следующему слою, и этот процесс будет повторяться, пока вы не дойдете до действия.

Если одновременно отображаются два или более фрагментов, только один из них является основным фрагментом навигации. Если вы назначите фрагмент основным, предыдущий фрагмент перестанет быть таковым. Если в предыдущем примере вы установите фрагмент сведений в качестве основного фрагмента навигации, обозначение основного фрагмента будет удалено.

Поддержка нескольких обратных стеков

В некоторых случаях приложению может потребоваться поддержка нескольких обратных стеков. Распространённый пример – если в вашем приложении используется нижняя панель навигации. FragmentManager позволяет поддерживать несколько обратных стеков с помощью методов saveBackStack() и restoreBackStack(). Эти методы позволяют переключаться между обратными стеками, сохраняя один и восстанавливая другой.

saveBackStack() работает аналогично вызову popBackStack() с необязательным параметром name: указанная транзакция и все последующие транзакции в стеке извлекаются. Разница в том, что saveBackStack() сохраняет состояние всех фрагментов в извлеченных транзакциях.

Например, предположим, что вы ранее добавили фрагмент в обратный стек, зафиксировав FragmentTransaction с помощью 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();

В этом случае вы можете сохранить транзакцию фрагмента и состояние ExampleFragment, вызвав saveBackStack():

Kotlin

supportFragmentManager.saveBackStack("replacement")

Java

supportFragmentManager.saveBackStack("replacement");

Вы можете вызвать restoreBackStack() с тем же параметром name, чтобы восстановить все извлеченные транзакции и все сохраненные состояния фрагментов:

Kotlin

supportFragmentManager.restoreBackStack("replacement")

Java

supportFragmentManager.restoreBackStack("replacement");

Предоставление зависимостей фрагментам

При добавлении фрагмента вы можете создать его экземпляр вручную и добавить его в 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();

При фиксации транзакции фрагмента используется созданный вами экземпляр фрагмента. Однако при изменении конфигурации действие и все его фрагменты удаляются, а затем создаются заново с наиболее подходящими ресурсами Android. FragmentManager обрабатывает все это за вас: он воссоздает экземпляры ваших фрагментов, прикрепляет их к хосту и воссоздает состояние обратного стека.

По умолчанию FragmentManager использует FragmentFactory, предоставляемый фреймворком, чтобы создать экземпляр фрагмента. Эта фабрика по умолчанию использует рефлексию, чтобы найти и вызвать конструктор без аргументов для вашего фрагмента. Это означает, что вы не можете использовать эту фабрику по умолчанию для предоставления зависимостей вашему фрагменту. Это также означает, что любой пользовательский конструктор, который вы использовали для создания фрагмента в первый раз, не используется при повторном создании по умолчанию.

Чтобы предоставить зависимости фрагменту или использовать собственный конструктор, создайте собственный подкласс FragmentFactory, а затем переопределите FragmentFactory.instantiate. Затем вы можете переопределить фабрику по умолчанию для FragmentManager, указав собственную фабрику, которая будет использоваться для создания экземпляров фрагментов.

Предположим, у вас есть DessertsFragment, который отвечает за отображение популярных десертов в вашем родном городе, и этот DessertsFragment зависит от класса DessertsRepository, который предоставляет ему информацию, необходимую для отображения правильного пользовательского интерфейса вашему пользователю.

Вы можете определить DessertsFragment так, чтобы в его конструкторе требовался экземпляр DessertsRepository.

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.

    ...
}

Простая реализация FragmentFactory может выглядеть следующим образом:

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

В этом примере создается подкласс FragmentFactory, переопределяющий метод instantiate(), чтобы предоставить собственную логику создания фрагментов для DessertsFragment. Другие классы фрагментов обрабатываются в режиме работы по умолчанию с помощью FragmentFactory и super.instantiate().

Затем вы можете указать MyFragmentFactory в качестве фабрики, которая будет использоваться при создании фрагментов приложения, задав свойство в FragmentManager. Это свойство необходимо задать до вызова метода super.onCreate(), чтобы при повторном создании фрагментов использовался класс MyFragmentFactory.

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

Если задать FragmentFactory в объекте activity, это переопределит создание фрагментов во всей иерархии фрагментов объекта activity. Другими словами, childFragmentManager любых дочерних фрагментов, которые вы добавляете, использует пользовательскую фабрику фрагментов, установленную здесь, если она не переопределена на более низком уровне.

Тестирование с помощью FragmentFactory

В архитектуре с одним действием тестируйте фрагменты изолированно с помощью класса FragmentScenario. Поскольку вы не можете полагаться на пользовательскую логику onCreate вашего действия, вы можете вместо этого передать FragmentFactory в качестве аргумента в тест фрагментов, как показано в следующем примере:

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

Подробную информацию о тестировании и примеры вы найдете в статье Тестирование фрагментов.