Неочевидное про Fragment API. Часть 2. (Не) создаем инстанс

0f04c2e43ba349a8b25fec1ab21175a1.png

Всем привет! Меня зовут Максим Бредихин, я Android-разработчик в Тинькофф. А это — вторая статья серии об интересных моментах из Fragment API, о которых вы, возможно, не знали.

  • Часть 1. Транзакции

  • Часть 2. (Не) создаем инстанс (вы находитесь здесь)

  • Часть 3. Навигация (coming soon)

  • Часть 4. Анимации и меню (coming soon)

Готовьте вкусности, сегодня я расскажу, как (не) создавать новые инстансы фрагментов.

Fragment в XML

Для работы с фрагментами не обязательно использовать FragmentManager напрямую. Если у нас есть стартовый фрагмент, достаточно указать его в XML через атрибут name у контейнера.



Дополнительно ничего делать не нужно, но под капотом без FragmentManager не обошлось. Вся магия происходит в четыре этапа:

  1. FragmentContainerView в конструкторе берет FragmentManager из родительского контекста. Если контейнер фрагмента находится в разметке Activity, будет использован supportFragmentManager, а если в разметке фрагмента — childFragmentManager.

  2. С помощью FragmentFactory создается инстанс фрагмента, указанного в поле android:name.

  3. Сразу после этого и до начала транзакции у фрагмента вызывается коллбэк Fragment.onInflate(Context, AttributeSet, Bundle?).

  4. Совершается транзакция.

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, этот метод будет вызван только один раз при первом создании инстанса фрагмента;

  • если мы предпочитаем старые подходы и используем в разметке, метод onInflate() будет полноценным методом жизненного цикла, который вызывается перед onAttach(). Несмотря на это, я не пропагандирую такой способ.

В остальных случаях метод не вызывается никогда. Его основное предназначение — достать аргументы из XML. Для этого нужно определить свои атрибуты для аргументов в ресурсах приложения. 

Создаем файл attrs.xml, прописываем нужные аргументы и указываем их в разметке.



   
       
   




На следующем шаге достаем аргументы из 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 у контейнера.

Важно! Создать фрагмент из 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, и className — полное имя нужного фрагмента. Исходя из имени определяем, какой фрагмент нам нужно создать, и возвращаем его. Если мы не знаем такого фрагмента, передаем управление родительской реализации.

Примерно так все выглядит super.instantiate() под капотом FragmentFactory:  

open fun instantiate(classLoader: ClassLoader, className: String): Fragment {
  try {
    val cls: Class = 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, 
  args: Bundle?
)

FragmentTransaction.add(
  @IdRes containerViewId: Int, 
  fragmentClass: Class, 
  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(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(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 для создания и сделать свой код немного чище.

В следующей статье мы посмотрим на новые и не очень фишки навигации между фрагментами. До встречи!

© Habrahabr.ru