
Всем привет! Меня зовут Максим Бредихин, я 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 для создания и сделать свой код немного чище.
В следующей статье мы посмотрим на новые и не очень фишки навигации между фрагментами. До встречи!
 
           
 
Veygard
Отличная статья!