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

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

Возможно, вы никогда не будете взаимодействовать с FragmentManager напрямую, если используете библиотеку навигации Jetpack , поскольку она работает с 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 может выглядеть так:

Котлин

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

Ява

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 . Вот пример:

Котлин

supportFragmentManager.commit {
   replace<ExampleFragment>(R.id.fragment_container)
   setReorderingAllowed(true)
   addToBackStack(null)
}
...
val fragment: ExampleFragment =
        supportFragmentManager.findFragmentById(R.id.fragment_container) as ExampleFragment

Ява

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 .

Котлин

supportFragmentManager.commit {
   replace<ExampleFragment>(R.id.fragment_container, "tag")
   setReorderingAllowed(true)
   addToBackStack(null)
}
...
val fragment: ExampleFragment =
        supportFragmentManager.findFragmentByTag("tag") as ExampleFragment

Ява

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 которого имеет основной контроль.

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

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

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

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

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

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

Например, предположим, что вы ранее добавили фрагмент в задний стек, зафиксировав FragmentTransaction с помощью addToBackStack() , как показано в следующем примере:

Котлин

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

Ява

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

Котлин

supportFragmentManager.saveBackStack("replacement")

Ява

supportFragmentManager.saveBackStack("replacement");

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

Котлин

supportFragmentManager.restoreBackStack("replacement")

Ява

supportFragmentManager.restoreBackStack("replacement");

Предоставьте зависимости вашим фрагментам

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

Котлин

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

Ява

// 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 .

Котлин

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

Ява

public class DessertsFragment extends Fragment {
    private DessertsRepository dessertsRepository;

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

    // Getter omitted.

    ...
}

Простая реализация вашей FragmentFactory может выглядеть примерно так.

Котлин

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

Ява

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 используется при воссоздании ваших фрагментов.

Котлин

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

Ява

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 в действии переопределяет создание фрагментов во всей иерархии фрагментов действия. Другими словами, childFragmentManager любых добавляемых вами дочерних фрагментов использует заводскую настройку пользовательского фрагмента, если она не переопределена на более низком уровне.

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

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

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

Подробную информацию об этом процессе тестирования и полные примеры см. в разделе Тестирование фрагментов .