Если вы интересуетесь разработкой под Android, то, скорее всего, слышали о Telegram-канале «Android Broadcast» с ежедневными новостями для Android-разработчиков и одноимённом YouTube-канале. Этот пост — текстовая версия видео.

Совсем недавно вышло одно из крупнейших обновлений Android Jetpack за последнее время. В рамках него множество библиотек вышли в новую стабильную фазу, но самым интересным релизом в рамках этого обновления я считаю версию фрагментов 1.4.0. Мы получили работу с несколькими Back Stack, новый менеджер состояний, проверки работы Fragment во время работы приложения, улучшенные анимации. 

Меня зовут Кирилл Розов. Я занимаюсь техпиаром в Surf, а также являюсь Android & Kotlin GDE. В статье расскажу про изменения, которые произошли в Fragment 1.4, и покажу, как с ними работать.

FragmentContainerView.getFragment()

Одно из минорных изменений в этом релизе — возможность получать Fragment из FragmentContainerView. Раньше, чтобы найти Fragment, нужно было вызвать FragmentManager.findFragmentById(int) либо FragmentManager.findFragmentById(String). Проблема такого подхода в том, что приходится выполнять приведение типа к необходимому. 

В AndroidX Fragment 1.3 появился специальный ViewGroup для добавления в него Fragment. Он получил новый метод FragmentConteinerView.getFragment(), который позволяет получить добавленный в него Fragment. Каст для выходного типа производить не придётся.

val fragmentContainer: FragmentContainerView = …
val homeFragment: HomeFragment = fragmentContainer.getFragment()

Новый менеджер состояний

В версии Android Fragment 1.4 удалили API, которое позволяло включать новый менеджер состояний. Новый менеджер состояний появился в предыдущей версии библиотеки, и теперь стал основным и единственным. 

Новый менеджер состояний упрощает работу с менеджментом состояний Fragment, из-за которого было много багов. Также он заложил основы для новой функциональности. Например, для множественного back stack, который позволит решать задачи без костылей. 

Если вам хочется узнать больше про рефакторинг Fragment, рекомендую прочитать статью Ian Lake «Fragments: rebuilding the internals». Несмотря на изменения под капотом, ожидаемое поведение Fragment и API не изменилось.

FragmentStrictMode

Fragment в последнее время получает сильное развитие: он явно останется с нами надолго.

Довольно сложно понять, как правильно работать с Fragment API, и уследить за всеми изменениями. Уже была огромная кодовая база, но автоматического инструмента для миграции на новые API в AndroidX Fragment не было. Очень логичным стало бы появление какого-то API, способного это отслеживать. И вот его представили — встречайте FragmentStrictMode.

Чтобы начать работу, надо задать политику проверки во FragmentManager:

supportFragmentManager.strictModePolicy =
     FragmentStrictMode.Policy.Builder()
         // Настраиваем реакцию на нарушения
         .penaltyDeath()
         .penaltyLog()
         // настраиваем какие нарушения отслеживать
         .detectFragmentReuse()
         .detectTargetFragmentUsage()
         .detectWrongFragmentContainer()
         .detectSetUserVisibleHint()
         // настраиваем исключения
         .allowVialotation(HomeFragment::class.java, WrongFragmentContainerViolation::class.java)
         // Создаём объект политик
         .build()

ВАЖНО! Объект политик задаётся только для текущего FragmentManager и не будет наследоваться дочерними FragmentManager.

Варианты реакций на нахождение нарушений 

penaltyDeath() — реакция, при которой приложение будет падать с ошибкой при нахождении уязвимости. Крайне не рекомендуется использовать в продакшене!

Реакция penaltyLog() аналогична предыдущей, но вместо крэша будет сообщение в Logcat. 

Третий тип реакций — слушатель.

FragmentStrictMode.Policy.Builder()
     .penaltyListener() { vialotation: Violation ->  …  }

Я рекомендую использовать слушатель, чтобы в продакшн-сборках отслеживать ошибки и позже исправлять их. Это легко сделать с Firebase Crashlytics, ведь Violation — это подкласс класса Throwable.

FragmentStrictMode.Policy.Builder()
     .penaltyListener() { vialotation -> 
       Firebase.crashlytics.recordException(vialotation)
     }

Нарушения

Что можно отслеживать? Сейчас доступно 6 ошибок.

detectFragmentReuse() — отслеживание повторного использования Fragment, который был удалён из FragmentManager.

val firstFragment =
     fragmentManager.findFragmentByTag(FRAGMENT_TAG_FIRST) ?: return  
fragmentManager.commit {
     remove(firstFragment)
}

fragmentManager.commit {
     // Добавление Fragment, удаленного ранее
     add(R.id.fragment_container, firstFragment, FRAGMENT_TAG_FIRST)
}

detectFragmentTagUsage() — использование тегов при добавлении Fragment из XML.

<fragment
     android:name="dev.androidbroadcast.FirstFragment"
     android:tag="first"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     />

detectWrongFragmentContainer() срабатывает при попытке добавить Fragment не во FragmentContainerView, который предназначен для работы с ним.

Остальные существующие проверки нарушений нужны, чтобы предотвращать использование deprecated API:

Multi Back Stack

Главной изюминкой фрагментов 1.4 стала поддержка множественного Back Stack. 

В чём суть: есть приложение с AppBar или NavBar. Согласно руководству по Material Design, при переключении табов навигации мы не сохраняем историю. Но заказчики, дизайнеры и менеджеры считают, что сохранять её необходимо — как на iOS. 

Раньше, чтобы реализовать это на Fragment в Android, нужно было делать костыли. С приходом Androidx Fragment 1.4 появилось новое API по сохранению транзакций Fragment и их восстановлению. Давайте разбираться, как это выполняется.

// Добавляем начальный Fragment
supportFragmentManager.commit {
     setReorderingAllowed(true)
     replace<HomeFragment>(R.id.fragment_container)
}

// Добавляем Fragment и помещаем в back stack
supportFragmentManager.commit {
     setReorderingAllowed(true)
     replace<ProfileFragment>(R.id.fragment_container)
     addToBackStack("my_profile")
}

// Добавляем ещё один Fragment и помещаем в back stack
supportFragmentManager.commit {
     setReorderingAllowed(true)
     replace<EditProfileFragment>(R.id.fragment_container)
     addToBackStack("edit_profile") // добавляем
}

Всё, что было добавлено в back stack FragmentManager, можно теперь сохранить.

supportFragmentManager.saveBackStack("profile")

По итогу в стеке будет только HomeFragment. Все остальные транзакции откатятся, но сохранятся с меткой “profile”.  Можно восстановить эти транзакции позже с помощью вызова либо удалить.

// Восстановить back stack “profile” и применить все транзакции в нём
supportFragmentManager.restoreBackStack(“profile”)

// Удалить back stack “profile”
supportFragmentManager.clearBackStack(“profile”)

API Fragment Multiple back stacks уже интегрировано с Jetpack Navigation 2.4.0. Если вам хочется больше узнать о том, как всё это работает, прочитайте статью Ian Lake «Multiple back stacks».

Новые проверки Android Lint

  • UseGetLayoutInflater — использование неправильного LayoutInflater в DialogFragment.

  • DialogFragmentCallbacksDetector — переопределение callback у Dialog, а не DialogFragment.

  • FragmentAddMenuProvider — использование неверного Lifecycle при использовании MenuProvider.

  • DetachAndAttachFragmentInSameFragmentTransaction — выполнение attach и detach для одного и того же Fragment в рамках одной транзакции.

Рекомендую запустить анализ кода Android Lint после обновления Fragment в проекте.


Новый релиз Fragment оказался достаточно интересным. Мы получили множественный Back Stack и действительно важные фичи. 

Но я помню то обещание, которое Ian Lake давал два года назад, в 2019 году, на Android Dev Summit. Он рассказывал о планах упросить работу с менеджментом жизненного цикла (ЖЦ) Fragment и составить только один ЖЦ. В теории это упростит взаимодействие в Fragment. Непонятно, что будет в этом случае с retain instance Fragment: надо подождать и посмотреть. 

Чего вы ждете от фрагментов, что вам хотелось бы видеть? Оставляйте комментарии.

Комментарии (4)


  1. quaer
    16.12.2021 17:58

    В чём суть: есть приложение с AppBar или NavBar. Согласно руководству по Material Design, при переключении табов навигации мы не сохраняем историю. ...

    Раньше, чтобы реализовать это на Fragment в Android, нужно было делать костыли.

    О какой именно истории идёт речь и о каких костылях?

    Что-то поменялось с передачей данных межд фрагментами с помощью NavController? Есть какой-то внятный спооб однообразно передать данные следующему фрагменту и из него обратно предыдущему при откате нажатием кнопки "назад"?


    1. kirich1409 Автор
      16.12.2021 18:09

      То что множество приложений не следуют гайдлауну Material и реализуют историю с между табави BottomNavigation


      1. quaer
        16.12.2021 18:36

        Что такое "история между табами"?


        1. kirich1409 Автор
          18.12.2021 14:06

          Можете посмотреть здесь https://material.io/components/bottom-navigation#behavior