Фрагменты в андроид разработке стали привычным способом написания ui и со временем, для удобства разработки, появилось много нового функционала. Один из таких примеров - использование своей реализации FragmentFactory. Об этом я и хотел бы поговорить.
Для чего нужно использовать фабрику фрагментов?
Часто бывает, что при создании во фрагмент нужно передать какие-то параметры, это может быть ссылка на объект, от которого зависит фрагмент или же, в самом простом случае, id контента, который нужно отобразить. Любой андроид разработчик знает, что просто передать в конструктор фрагмента нужные параметры не получится, так как при пересоздании фрагмента используется дефолтный конструктор. Стандартный способ для передачи id, положить его в аргументы фрагмента.
class FragmentA : Fragment() {
private val mContentId: Long?
get() = arguments?.getLong(CONTENT_ID_ARG)
companion object {
private const val CONTENT_ID_ARG = "content_id_arg"
fun newInstance(contentId: Long): FragmentA {
return FragmentA().apply {
arguments = Bundle().apply {
putLong(CONTENT_ID_ARG, contentId)
}
}
}
}
}
Если нужно передавать сразу несколько аргументов, то вся процедура связанная упаковкой аргумента в бандл повторяется. Это бойлерплейт, но такое android sdk, иногда приходится делать лишние действия. Сравнительно недавно, с появлением androidx, стал доступен альтернативный способ - создания фрагмента при помощи своей реализации FragmentFactory. Статьи про такой подход не так много, но я приведу пару источников, чтобы не повторять авторов.
https://developer.android.com/reference/androidx/fragment/app/FragmentFactory
https://proandroiddev.com/android-fragments-fragmentfactory-ceec3cf7c959
https://medium.com/capital-one-tech/android-fragmentfactory-75823af015fd
Вкратце, все что нам нужно - унаследоваться от класса FragmentFactory и переопределить его метод instantiate(). По имени класса мы можем определить какой экземпляр фрагмента нам нужно создать. В коде это все выглядит так:
class MyFragmentFactory(
private val fragmentAProvider: () -> Fragment
): FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
return when(className) {
FragmentA::class.java.name -> fragmentAProvider()
else -> super.instantiate(classLoader, className)
}
}
}
И тогда, при создании фрагмента, достаточно будет передать нужные параметры в конструктор.
class FragmentA(
private val contentId: Long
) : Fragment(R.layout.fragment_a)
Код в активити.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val contentId = intent.extras?.getLong(CONTENT_ID_ARG) ?: throw IllegalArgumentException(
"Extras should contains contentId"
)
supportFragmentManager.fragmentFactory = MyFragmentFactory {
FragmentA(contentId)
}
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState=!= null) {
supportFragmentManager.commit {
add(R.id.container, FragmentA::class.java, null)
}
}
}
companion object {
const val CONTENT_ID_ARG = "content_id_arg"
}
}
Так же это будет работать и при создании фрагмента через FragmentContainerView в xml.
Хочу обратить внимание, что фабрика устанавливается до вызова super.onCreate(), иначе будет использоваться дефолтная. Еще один важный момент, про который нужно помнить, это то, что фабрика будет установлена для всех фрагментов, которые находятся в данной активити, то есть каждый childFragmentManager теперь будет использовать эту фабрику.
Теперь, если появляется еще один фрагмент в активити, то достаточно добавить условие в нашу фабрику фрагментов.
class MyFragmentFactory(
private val fragmentAProvider: () -> Fragment,
private val fragmentBProvider: () -> Fragment
): FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
return when(className) {
FragmentA::class.java.name -> fragmentAProvider()
FragmentB::class.java.name -> fragmentBProvider()
else -> super.instantiate(classLoader, className)
}
}
}
От лишнего кода во фрагменте мы избавились, но если у нас в приложении несколько активити, или, увы, как часто бывает, создается в каждом фиче-модуле своя активити, которая отвечает за отображение нескольких фрагментов (для проектов с сингл-активити проблема не актуальная), удобство уже кажется не таким явным, ведь нам для каждой активити нужно будет создавать подобный класс. Давайте попробуем исправить это при помощи возможности котлина создавать декларативное api.
Начнем с описания того, что мы хотим получить в итоге: мне бы хотелось, вместо создания класса, такую функцию, которая в колбеке предоставляет возможность декларировать все фрагменты, которые мы хотим создавать в рамках взаимодействия с данной активити. В конечном итоге нам будет достаточно написать такой код, чтобы описать все необходимые провайдеры фрагментов:
fragmentFactory {
add { FragmentA(contentId) }
add { FragmentB() }
}
Итак, приступим:
Первое что нам нужно - это функция fragmentFactory, которая будет создавать и возвращать объект FragmentFactory, также переопределим сразу метод instantiate.
fun fragmentFactory() = object : FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
return super.instantiate(classLoader, className)
}
}
Далее нам понадобится класс, объект которого будет отвечать за хранение всех фабрик фрагментов. Назовем его FragmentProviders. У объекта этого класса должна быть возможность добавлять фабрики фрагментов и получать их по ключу.
class FragmentProviders {
private val mProviders = mutableMapOf<String, () -> Fragment>()
fun add(className: String, provider: () -> Fragment) {
mProviders[className] = provider
}
operator fun get(className: String) = mProviders[className]
}
Создадим объект этого класса в нашей FragmentFactory, а в параметры метода fragmentFactory передадим функцию, при вызове которой FragmentProviders заполнится всеми необходимыми провайдерами фрагментов.
fun fragmentFactory(provider: FragmentProviders.() -> Unit) = object : FragmentFactory() {
private val mProviders = FragmentProviders()
init {
mProviders.provider()
}
override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
return mProviders[className]?.invoke() ?: super.instantiate(classLoader, className)
}
}
Последние штрихи:
нужно создать функцию-расширения для FragmentProviders, при помощи которой можно будет удобно добавлять фрагменты, не думая о передаче ключа.
inline fun <reified T : Fragment> FragmentProviders.add(noinline provider: () -> T) {
add(T::class.java.name, provider)
}
также можно пометить функцию fragmentFactory ключевым словом inline, чтобы не создавать лишний объект и добавить функцию-расширение для активити и фрагмента. Весь код:
inline fun AppCompatActivity.fragmentFactory(crossinline provider: FragmentProviders.() -> Unit) {
supportFragmentManager.fragmentFactory = createFragmentFactory(provider)
}
inline fun Fragment.fragmentFactory(crossinline provider: FragmentProviders.() -> Unit) {
childFragmentManager.fragmentFactory = createFragmentFactory(provider)
}
inline fun createFragmentFactory(crossinline provider: FragmentProviders.() -> Unit) =
object : FragmentFactory() {
private val mProviders = FragmentProviders()
init {
mProviders.provider()
}
override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
return mProviders[className]?.invoke() ?: super.instantiate(classLoader, className)
}
}
class FragmentProviders {
private val mProviders = mutableMapOf<String, () -> Fragment>()
fun add(className: String, provider: () -> Fragment) {
mProviders[className] = provider
}
operator fun get(className: String) = mProviders[className]
}
inline fun <reified T : Fragment> FragmentProviders.add(noinline provider: () -> T) {
add(T::class.java.name, provider)
}
Код в активити:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val contentId = intent.extras?.getLong(CONTENT_ID_ARG) ?: throw IllegalArgumentException(
"Extras should contains contentId"
)
fragmentFactory {
add { FragmentA(contentId) }
add { FragmentB() }
}
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) {
supportFragmentManager.commit {
add(R.id.container, FragmentA::class.java, null)
}
}
}
companion object {
const val CONTENT_ID_ARG = "content_id_arg"
}
}
Теперь без труда можно создавать новые фабрики фрагментов и не нужно выполнять лишних действий с Bundle для передачи параметров.
Фабрики фрагментов, как мне кажется, в реальных проектах используются редко, в основном потому, что они появились не так давно. Многие просто про них ничего не слышали, и привыкли работать “по старинке”.Часто это может быть даже удобнее. Но есть ситуации, когда FragmentFactory все таки может пригодиться. И если, вруг, FragmentFactory приходится использовать чаще одного раза, подход, описанный в статье, поможет упростить создание и добавить немного декларативности в ваш ui код.
Комментарии (5)
kavaynya
16.01.2023 11:19+1Выглядит красиво и удобно. Но как быть с передачей аргументов во фрагменты, если они были получены после создания Activity (например, через интернет запроса)?
AlexeyMinay Автор
16.01.2023 11:35Тут могу предложить, либо передавать в add фабрику фрагмента, либо комбинировать два способа и передать аргумент, который узнается после создания фрагмента, через способ с бандл. Возможно, если это не подходит, отказаться от использования неудобного апи. Мне лично приходилось два раза использовать FragmentFactory и оба раза аргументы были известны при создании активити.
kavaynya
16.01.2023 12:16Классический вариант с arguments мне известен. И он работает на 100%.
Фабрики, что добавил Google для Fragments, во факту такую задачу решить не могут, так как в большинстве случаев передаваемые аргументы не известны, в момент создания фрагмента (при условии, что вы используете Single Activity). Если у вас каждый Fragment имеет свою Activity и вы в Activity передаете эти самые аргументы, то такой способ подходит отлично.
patrikon
Спасибо за статью.
Но вот что интересно - используется ли вообще где-то ещё подход с использование "чистого"
supportFragmentManager, чтобы данный подход применять? Мне кажется везде либо какие-то либы-фреймворки-обёртки, либо
jetpack navigation либо вовсе новомодный compose.AlexeyMinay Автор
Хороший вопрос, добавил опросник чтобы посчитаться