Упрощаем создание FragmentFactory при помощи dsl котлина

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Фрагменты в андроид разработке стали привычным способом написания 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 код.

Источник: https://habr.com/ru/post/710474/


Интересные статьи

Интересные статьи

«Порезать детальки», «прогреть жало», «к станку», «царский корпус» — такое вероятнее встретить в разговоре с хорошими знакомыми, чем на официальном сайте. Если конечно это не сайт «Амперки». Когда-то ...
Привет, меня зовут Владимир Кононенко и я – руководитель управления внедрения в Группе компаний ОТР. Всем, кто работает с информационными системами (ИС), знакома такая история: на определенном этапе ж...
В этой статье мы популярно объясняем на собственном опыте как организовать массовую выгрузку, обработку и загрузку фотографий товаров из Bitrix, используя Python и минимальное количество SQL. Для проч...
Если вам приходилось руководить разработкой программного продукта, вы наверняка задумывались — как помочь команде двигаться быстрее? И как вообще понять, насколько быстро вы движетесь...
Автокэширование в 1с-Битрикс — хорошо развитая и довольно сложная система, позволяющая в разы уменьшить число обращений к базе данных и ускорить выполнение страниц.