Понимание фрагментов в Android: часть 2
30 октября 2022 г.В этой статье мы разберем интересные моменты Fragment API, думаю, что это будет интересно всем разработчикам, которые разрабатывают приложение для Android.
Во-первых, вам нужно добавить зависимости:
dependencies {
def fragment_version = "1.5.4"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
}
Фрагмент-ktx
Диспетчер фрагментов
Теперь вы можете описывать транзакции в стиле DSL, а функции beginTransaction()
и commit()
или commitAllowStateLoss()
вызываются при капюшон:
fun FragmentManager.commit(
allowStateLoss: Boolean = false,
block: FragmentTransaction.() -> Unit
)
Пример:
fragmentManager.commit {
// transaction
}
Фрагментная транзакция
Добавлена замена для перегрузок метода FragmentTransaction.add(Int, Class<out Fragment>, Bundle?)
. Аналогичное расширение было добавлено для FragmentTransaction.replace()
:
fun <reified T: Fragment> FragmentTransaction.add(
containerId: Int,
tag: String? = null,
args: Bundle? = null
): FragmentTransaction
fun <reified T: Fragment> FragmentTransaction.replace(
containerId: Int,
tag: String? = null,
args: Bundle? = null
): FragmentTransaction
Пример:
fragmentManager.commit {
val args = bundleOf("key" to "value")
add<FragmentA>(R.id.fragment_container, "tag", args)
replace<FragmentB>(R.id.fragment_container)
}
Оптимизация транзакций
Оптимизация — очень важная вещь. Чтобы понять, как FragmentManager может сделать все за нас, давайте попробуем оптимизировать транзакции вручную.
fragmentManager.commit {
add<FragmentA>(R.id.fragment_container)
replace<FragmentB>(R.id.fragment_container)
replace<FragmentC>(R.id.fragment_container)
}
Как ускорить транзакцию? При сложнейшем техническом анализе мы видим, что по его результатам пользователь увидит FragmentC
. Мы люди простые и просто выбрасываем лишние два действия, сразу показывая FragmentC
. Другой пример — две транзакции, выполняемые одна за другой:
// 1
fragmentManager.commit {
add<FragmentA>(R.id.fragment_container)
}
// 2
fragmentManager.commit {
replace<FragmentB>(R.id.fragment_container)
}
В этом случае мы могли бы прервать добавление FragmentA
и немедленно добавить FragmentB
. Мы не можем этого сделать, но проблема скорее теоретическая. Все вышеперечисленное FragmentManager может делать самостоятельно. Нам просто нужно разрешить это, добавив setReorderingAllowed(true)
в транзакцию, которую мы хотим оптимизировать.
fragmentManager.commit {
setReorderingAllowed(true)
add<FragmentA>(R.id.fragment_container)
replace<FragmentB>(R.id.fragment_container)
replace<FragmentC>(R.id.fragment_container)
}
Во-вторых, нужно установить фокус, во-первых, потому что это ее разрешение прерывать, а во-вторых, в свою очередь, нужно полностью контролировать:
// 1
fragmentManager.commit {
setReorderingAllowed(true)
add<FragmentA>(R.id.fragment_container)
}
// 2
fragmentManager.commit {
replace<FragmentB>(R.id.fragment_container)
}
По сути, мы позволяем FragmentManager
вести себя лениво и не выполнять лишние команды, что дает некоторый выигрыш в производительности. Кроме того, это помогает правильно обрабатывать анимацию, переходы и задний стек.
Стоит помнить, что оптимизированный FragmentManager
может:
- не создавать фрагмент, если он заменяется в той же транзакции;
- прервать жизненный цикл в любой момент до
RESUMED
, если началась транзакция по замене добавленного фрагмента; - заставить
onCreate()
нового фрагмента вызываться передonDestroy()
старого.
Во время транзакции есть несколько способов управления жизненным циклом фрагмента, в некоторых случаях это может быть полезно.
Изменить видимость
Мы можем скрывать и отображать фрагмент, не меняя его состояния жизненного цикла. Это поведение похоже на View.visibility = View.GONE
и View.visibility = View.VISIBLE
.
Мы можем просто спрятать контейнер. Это так, но спрятать контейнер в бэкстеке не получится, а транзакцию с подобной командой легко. Чтобы скрыть фрагмент от посторонних глаз, достаточно вызвать метод FragmentTransaction.hide(Fragment)
:
fragmentManager.commit {
setReorderingAllowed(true)
fragmentManager.findFragmentById(R.id.fragment_container)?.let {
hide(it)
}
}
Чтобы показать его снова, вам нужно вызвать метод FragmentTransaction.show(Fragment)
:
fragmentManager.commit {
setReorderingAllowed(true)
fragmentManager.findFragmentById(R.id.fragment_container)?.let {
show(it)
}
}
Уничтожение представления
Мы можем уничтожить представление фрагмента, но не сам фрагмент
, вызвав метод FragmentTransaction.detach(Fragment)
. В результате такой транзакции фрагмент перейдет в состояние STOPPED
:
fragmentManager.commit {
setReorderingAllowed(true)
fragmentManager.findFragmentById(R.id.fragment_container)?.let {
detach(it)
}
}
Чтобы воссоздать представление фрагмента, просто вызовите метод FragmentTransaction.attach(Fragment)
:
fragmentManager.commit {
setReorderingAllowed(true)
fragmentManager.findFragmentById(R.id.fragment_container)?.let {
attach(it)
}
}
Фабрика фрагментов
В 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) {
…
}
}
LayoutId в конструкторе
Давайте вспомним, как мы учились работать с фрагментами. Мы создаем класс, создаем файл разметки и расширяем его в onCreateView()
:
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = inflater.inflate(R.layout.fragment_example, container, false)
Мы набирали эти знакомые строки сотни раз, но в версии 1.1.0 Fragments
ребята из 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
}
Обзор
В этой статье были рассмотрены возможности и новые возможности создания фрагментов, а также способы передачи LayoutId в конструктор фрагментов. В следующей части статьи мы рассмотрим анимации при переходе и закрытии Фрагмента.
Оригинал