Всем привет! Меня зовут Максим Бредихин, я Android-разработчик в Тинькофф. А это — вторая статья серии об интересных моментах из Fragment API, о которых вы, возможно, не знали.
-
Часть 2. (Не) создаем инстанс (вы находитесь здесь)
-
Часть 3. Навигация (coming soon)
-
Часть 4. Анимации и меню (coming soon)
Готовьте вкусности, сегодня я расскажу, как (не) создавать новые инстансы фрагментов.
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 для создания и сделать свой код немного чище.
В следующей статье мы посмотрим на новые и не очень фишки навигации между фрагментами. До встречи!
Автор: Бредихин Максим