Небольшой гайд по работе с фрагментами в android studio.
Фрагменты чем то напоминают фреймы в html. Фрагменты обладают многими возможностями, которые есть у активити (Activity), имеют свой lifecycle, также работают с viewModel и т.п. И конечно же фрагмент имеет собственный шаблон (layout).
Но при этом, фрагмент можно встроить в другой шаблон (используя контейнер FragmentContainerView) и запрограммировать логику активити или другого фрагмента, чтобы он подключал нужный фрагмент, где инкапсулирована требуемая логика.
Пусть у нас в шаблоне активити есть вот такой элемент:
1 2 3 |
<androidx.fragment.app.FragmentContainerView android:id="@+id/item_container" ... /> |
Позиционирование и размеры, которые я опустил, будут определятся уже контейнером шаблона активити, нам для определенности нужен только id компонента.
Создание фрагмента
Также мы создали фрагмент (New -> Fragment -> FragmentBlank), который назвали ItemFragment. IDE создаст два файла — в одном из них будет наследник класса Fragment, а в другом — шаблон фрагмента.
Чем хорошо создание фрагмента через IDE, это тем, что мы получаем заготовку с использованием всех новых best — практик, которые приняты на данный момент.
Рассмотрим этот шаблон:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
package ... import ... // TODO: Rename parameter arguments, choose names that match // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER private const val ARG_PARAM1 = "param1" private const val ARG_PARAM2 = "param2" /** * A simple [Fragment] subclass. * Use the [itemFragment.newInstance] factory method to * create an instance of this fragment. */ class itemFragment : Fragment() { // TODO: Rename and change types of parameters private var param1: String? = null private var param2: String? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) arguments?.let { param1 = it.getString(ARG_PARAM1) param2 = it.getString(ARG_PARAM2) } } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment return inflater.inflate(R.layout.fragment_item, container, false) } companion object { /** * Use this factory method to create a new instance of * this fragment using the provided parameters. * * @param param1 Parameter 1. * @param param2 Parameter 2. * @return A new instance of fragment itemFragment. */ // TODO: Rename and change types and number of parameters @JvmStatic fun newInstance(param1: String, param2: String) = itemFragment().apply { arguments = Bundle().apply { putString(ARG_PARAM1, param1) putString(ARG_PARAM2, param2) } } } } |
Как видно из примера выше — IDE создает заготовку фрагмента с двумя входными параметрами. Параметры передаются через аргументы фрагмента в onCreate (а не аргументы конструктора класса), а для создания фрагмента, предлагается использовать «фабричный» подход — статический метод newInstance.
В onCreateView мы подключаем шаблон.
Еще одно важное событие — onViewCreated — для которого не создаётся перегрузки по умолчанию, но которое обычно переопределяется разработчиком.
1 2 3 4 5 6 7 8 |
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) initViews(view) ... } |
Здесь layout уже создан и готов к работе, и мы можем получаем контейнер шаблона в виде объекта view. В нем мы можем поискать прочие компоненты, которые мы объявили в шаблоне фрагмента:
1 2 3 4 5 6 |
private fun initViews(view: View) { with(view) { etName = findViewById(R.id.textEditName) ... } } |
Параметры фрагмента
Когда android пересоздаёт активити (а вместе с ней и все её фрагменты), например, при повороте экрана, то вызывается конструктор фрагмента без параметров. Потому передавать параметры в конструктор, хоть и удобно, но чревато вылетом приложения.
У активити используется очень похожий механизм, только там мы должны задать параметры при создании intent объекта.
В шаблоне от IDE продемонстрирован правильный подход к работе с параметрами.
Подключение фрагмента
Теперь нам нужно подключить фрагмент в соответствующий компонент активити (в нашем примере у него id = item_container).
Обычно это делают в onCreate вот так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_layout) val param1 = "..." val param2 = "..." if (savedInstanceState == null) { val fragment = itemFragment.newInstance(param1, param2) supportFragmentManager.beginTransaction() .add(R.id.item_container, fragment) .commit() } } |
Во первых видно, что используется supportFragmentManager, который требует начать транзакцию beginTransaction(), а в конце выполнить её commit(). Об этом механизме можно не думать, т.к. нам важна операция добавления.
1 |
.add(R.id.item_container, fragment) |
Мы указываем компонент контейнера по его ID и передаём фрагмент, воспользовавшись фабричным методом из шаблона, созданного IDE.
Но для чего делается проверка (?!) :
1 2 |
if (savedInstanceState == null) ... |
При пересоздании активити, андроид сам выполняет пересоздание фрагмента. Потому от нас требуется создать фрагмент только, если это первый запуск создания активити. Иначе, например, при повороте экрана, фрагмент будет создаваться дважды — в onCreate активити и платформой android.
Также лучше не использовать метод add, а заменить его на метод replace.
1 |
.replace(R.id.item_container, fragment) |
FragmentContainerView отобразит все контейнеры, добавленные в него (add). Так можно реализовать многослойный вывод, к примеру. При этом, каждый слой сможет иметь свою viewModel и шаблон и т.д.
1 2 3 4 5 |
supportFragmentManager .beginTransaction() .add(R.id.shop_item_container, fragment1) .add(R.id.shop_item_container, fragment2) .commit() |
Но обычно требуется подключить какой то конкретный фрагмент, а остальные (если они там были) удалить — используйте replace.
Навигация с фрагментами
Если важна история добавления фрагментов, т.е. есть какая то логика в этом для приложения, то можно добавлять фрагменты в стек навигации (т.н. BackStack).
Андроид при этом запоминает состояния в связке активити + фрагменты и позволяет вернуться к предыдущему состоянию при нажатии кнопки «назад».
Добавление фрагмента будет выглядеть вот так:
1 2 3 4 5 |
supportFragmentManager .beginTransaction() .replace(R.id.item_container, fragment) .addToBackStack(null) .commit() |
Чтобы выбросить с вершины стека имеющийся там фрагмент, можно вызвать
1 |
supportFragmentManager.popBackStack() |
addToBackStack позволяет именовать навигационные элементы (в примере передан null вместо имени), тогда при необходимости можно вернуться к состоянию бек-стека по его имени.
1 |
supportFragmentManager.popBackStack(name) |
Метод перегруженный и туда также можно передавать определенные флаги. Я думаю, достаточно этой информации, чтобы вы сами начали изучать доки при необходимости.
Завершение работы с фрагментом, обратная связь с активити
Когда логика работы требует спрятать фрагмент, заменить его другим фрагментом или что то сделать в прочих элементах активити (родительском компоненте), то надо как то дать знать активити об этом.
Можно пытаться сделать какие то связи между компонентами, но это ведет к проблемам проектирования. Т.е. мы можем получить ссылку на родительское активити, и вызвать в нем нужную нам функцию прямо из фрагмента, но делать так не рекомендуется. Очевидно, что это плохой подход.
Другой (тоже не верный) путь — это инициализировать какие то кол-беки при создании фрагмента. Это будет работать до тех пор пока андроид не пересоздаст фрагмент. Понятно, что он не выполнит повторно нужных инициализаций, и работать этот код перестанет.
Рекомендованный путь похож на первый вариант, но с некоторыми деталями. Мы должны объявить в нашем фрагменте интерфейс вроде следующего:
1 2 3 |
interface OnFragmentFinishedListener { fun onFinished() } |
Теперь, если мы хотим использовать фрагмент, родитель (активити) должен реализовать этот интерфейс.
1 2 3 4 5 6 7 8 9 |
class MainActivity : AppCompatActivity(), itemFragment.OnFragmentFinishedListener { override fun onFinished() { ... // какая то логика, например, откат назад по истории // это код слушателя события из фрагмента supportFragmentManager.popBackStack() } |
Во фрагменте, при прикреплении к родителю, мы добавим анализ: реализует ли наш родительский контейнер нужный нам интерфейс:
1 2 3 4 5 6 7 8 9 10 |
private lateinit var onFinishedListener: OnFragmentFinishedListener override fun onAttach(context: Context) { super.onAttach(context) if (context is OnFragmentFinishedListener) { onFinishedListener = context } else { throw RuntimeException("Activity must implement OnFragmentFinishedListener") } } |
Логика может быть разной — в данном случае фрагмент требует реализации данного интерфейса, в противном случае генерируется исключение.
А когда приходит момент вызова кол-бека, то выполняется код:
1 |
onFinishedListener.onFinished() |
И уже родитель решает, что с этим событием делать — спрятать фрагмент, заменить его другим, вывести какое то сообщение и т.п.