Понимание фрагментов в Android: часть 2

Понимание фрагментов в 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 в конструктор фрагментов. В следующей части статьи мы рассмотрим анимации при переходе и закрытии Фрагмента.


Оригинал