
Всем привет! Меня зовут Максим Бредихин, я Android-разработчик в Тинькофф. А это — вторая статья серии об интересных моментах из Fragment API, о которых вы, возможно, не знали.
Часть 2. (Не) создаем инстанс (вы находитесь здесь)
Готовьте вкусности, сегодня я расскажу, как (не) создавать новые инстансы фрагментов.
Fragment в XML
Для работы с фрагментами не обязательно использовать FragmentManager напрямую. Если у нас есть стартовый фрагмент, достаточно указать его в XML через атрибут name у контейнера.
<androidx.fragment.app.FragmentContainerView android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" android:name="com.example.ExampleFragment" /> <!--- Аналогично для <fragment> -->
Дополнительно ничего делать не нужно, но под капотом без FragmentManager не обошлось. Вся магия происходит в четыре этапа:
FragmentContainerView в конструкторе берет FragmentManager из родительского контекста. Если контейнер фрагмента находится в разметке Activity, будет использован
supportFragmentManager, а если в разметке фрагмента —childFragmentManager.С помощью FragmentFactory создается инстанс фрагмента, указанного в поле
android:name.Сразу после этого и до начала транзакции у фрагмента вызывается коллбэк
Fragment.onInflate(Context, AttributeSet, Bundle?).Совершается транзакция.
val containerFragment = fragmentManager.fragmentFactory.instantiate(context.classLoader, name) containerFragment.onInflate(context, attrs, null) fragmentManager.commit (allowStateLoss = true) { setReorderingAllowed(true) add(this, containerFragment, tag) }
Стоит разобраться с onInflate(). Он вызывается до начала транзакции и, следовательно, дергается до всех коллбэков жизненного цикла.

Первое и главное условие для вызова этого метода: фрагмент должен создаваться через XML. А дальше у нас две дороги:
если мы — модные, молодежные и современные разработчики, которые слушают Google и используют в качестве контейнера
FragmentContainerView, этот метод будет вызван только один раз при первом с��здании инстанса фрагмента;если мы предпочитаем старые подходы и используем
<fragment>в разметке, методonInflate()будет полноценным методом жизненного цикла, который вызывается передonAttach(). Несмотря на это, я не пропагандирую такой способ.
В остальных случаях метод не вызывается никогда. Его основное предназначение — достать аргументы из XML. Для этого нужно определить свои атрибуты для аргументов в ресурсах приложения.
Создаем файл attrs.xml, прописываем нужные аргументы и указываем их в разметке.
<!--- values/attrs.xml --> <resources> <declare-styleable name="ExampleFragment"> <attr name="myArgument" format="string" /> </declare-styleable> </resources> <!--- activity_main.xml --> <androidx.fragment.app.FragmentContainerView android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" android:name="com.example.ExampleFragment" app:myArgument="My argument value" />
На следующем шаге достаем аргументы из attrs в onInflate().
// ExampleFragment override fun onInflate(context: Context, attrs: AttributeSet, savedInstanceState: Bundle?) { super.onInflate(context, attrs, savedInstanceState) val attributes = context.obtainStyledAttributes(attrs, R.styleable.ExampleFragment) attributes.getString(R.styleable.ExampleFragment_myArgument)?.let { argumentValue -> // Кладем в arguments, чтобы не потерять их при смене конфигурации arguments = bundleOf("myArgument" to argumantValue) } attributes.recycle() }
Отойдем от аргументов и вспомним, что в транзакции при желании можно присвоить фрагменту tag. То же самое можно сделать через XML. Для этого достаточно указать android:tag у контейнера.
<androidx.fragment.app.FragmentContainerView android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" android:name="com.example.ExampleFragment" android:tag="The best fragment" />
Важно! Создать фрагмент из XML мы можем, только указав ID у контейнера, иначе упадем с
IllegalStateException. Это нужно для сохранения состояния при пересоздании View.
На момент Fragments: 1.5.0 с таким фрагментом можно совершать любые транзакции, доступные для обычных фрагментов. Главное — выбрать нужный FragmentManager и достать фрагмент через ID контейнера или указанный в XML тег.
fragmentManager.commit { setReorderingAllowed(true) fragmentManager.findFragmentById(R.id.container)?.let { remove(it) } }
FragmentFactory
В генах Android-разработчика прописано, что мы обязаны создавать фрагменты с пустым конструктором, чтобы система могла их самостоятельно пересоздать. Однако в версии Fragments 1.1.0 у нас появилась возможность контролировать создание инстансов фрагментов, в том числе добавлять любые параметры и зависимости в конструктор.
Для этого достаточно подменить стандартную реализацию FragmentFactory на свою, где мы сами себе цари и боги.
fragmentManager.fagmentFactory = MyFragmentFactory(Dependency())
Главное — успеть заменить реализацию до того, как она понадобится FragmentManager’у, то есть до первой транзакции и восстановления состояния после пересоздания. Чем раньше мы заменим негодную реализацию, тем лучше.
Для Activity лучшим сценарием будет замена:
до
super.onCreate();в блоке
init.
У фрагментов доступ к своему FragmentManager’у появляется не сразу. Поэтому подмену мы можем совершить только между onAttach() и onCreate() включительно, иначе увидим страшный красный текст в логах после запуска. Но важно помнить, что parentFragmentManager — это FragmentManager, через который совершили коммит. Следовательно, если в нем ранее заменили FragmentFactory, делать это во второй раз не нужно.
Теперь разберемся, как нам реализовать свою фабрику. Создаем класс, наследуемся от FragmentFactory и переопределяем метод instantiate().
class MyFragmentFactory( private val dependency: Dependency ) : FragmentFactory() { override fun instantiate(classLoader: ClassLoader, className: String): Fragment { return when(className) { FirstFragment::class.java.name -> FirstFragment(dependency) SecondFragment::class.java.name -> SecondFragment() else -> super.instantiate(classLoader, className) } } }
На вход получаем classLoader, который можно использовать для создания Class<out Fragment>, и className — полное имя нужного фрагмента. Исходя из имени определяем, какой фрагмент нам нужно создать, и возвращаем его. Если мы не знаем такого фрагмента, передаем управление родительской реализации.
Примерно так все выглядит super.instantiate() под капотом FragmentFactory:
open fun instantiate(classLoader: ClassLoader, className: String): Fragment { try { val cls: Class<out Fragment> = loadFragmentClass(classLoader, className) return cls.getConstructor().newInstance() } catch (java.lang.InstantiationException e) { … } }
Транзакции без создания Fragment
Кто-то может сказать: «FragmentFactory — штука классная, но для транзакций нам все равно нужны конкретные инстансы, так что пойду-ка я добавлю в свой фрагмент companion object». И он будет прав, но только если сидит на фрагментах до версии 1.2.0.
В этой версии нас избавили от необходимости создавать инстанс фрагмента в транзакции вручную, добавив дополнительные перегрузки методов FragmentTransaction.add():
FragmentTransaction.add( @IdRes containerViewId: Int, fragmentClass: Class<out Fragment>, args: Bundle? ) FragmentTransaction.add( @IdRes containerViewId: Int, fragmentClass: Class<out Fragment>, args: Bundle?, tag: String? )
Аналогичные методы добавили и для FragmentTransaction.replace(). Теперь мы можем сделать так:
fragmentManager.beginTransaction() .setReorderingAllowed(true) .add(R.id.container, ExampleFragment::class.java, null, "tag") .commit()
Или использовать fragment-ktx и расширение с reified-дженериком, которое я упоминал в первой части цикла.
fragmentManager.commit { setReorderingAllowed(true) add<ExampleFragment>(R.id.container, tag = "tag") }
Что еще круче, теперь мы можем передать аргументы сразу во время транзакции:
val args = bundleOf("arg" to "value") fragmentManager.beginTransaction() .setReorderingAllowed(true) .add(R.id.container, ExampleFragment::class.java, args) .commit()
Или с использованием fragment-ktx:
val args = bundleOf("arg", "value") fragmentManager.commit { setReorderingAllowed(true) add<ExampleFragment>(R.id.container, args = args) }
Во фрагменте нам останется только достать их как обычные аргументы:
class ExampleFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val someArg = requireArguments().getString("arg") // do something } }
LayoutId в конструкторе
Вспомним, как мы учились работать с фрагментами. Создаем класс, создаем файл разметки и «надуваем» его в onCreateView():
override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? = inflater.inflate(R.layout.fragment_example, container, false)
Мы сотни раз набирали эти родные сердцу строки, но в версии Fragments 1.1.0 ребята из Google решили, что больше не будут это терпеть. Они добавили фрагментам второй конструктор, принимающий на вход @LayoutRes, благодаря которому больше не нужно переопределять onCreateView().
class ExampleFragment : Fragment(R.layout.fragment_example)
А под капотом работает тот же самый бойлерплейт:
constructor(@LayoutRes contentLayoutId: Int) : this() { mContentLayoutId = contentLayoutId } open fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) : View? { if (mContentLayoutId != 0) { return inflater.inflate(mContentLayoutId, container, false) } return null; }
Чем меньше кода нам придется писать, тем лучше. Поэтому давайте не будем писать шаблонный код, если этого можно избежать.
Если вдруг вы до этого инициализировали View в onCreateView(), правильнее использовать специальный коллбэк onViewCreated(), вызываемый сразу после onCreateView().
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { button.setOnClickListener { // do something } // some view initialization }
Вместо заключения
Подошла к концу вторая часть цикла «Неочевидного о фрагментах». В этой статье мы разобрались с созданием фрагментов в XML, добавили зависимости в конструктор через FragmentFactory, узнали, что создавать фрагменты в транзакциях не обязательно, и избавились от небольшого кусочка бойлерплейта в нашем коде.
Теперь вы сможете использовать фрагменты без companion object для создания и сделать свой код немного чище.
В следующей статье мы посмотрим на новые и не очень фишки навигации между фрагментами. До встречи!
