Android — ViewPager2 — заменяем фрагменты на лету (программно)

от автора

Вдруг вам надо листать фрагменты через ViewPager2 и при этом подменять их динамически. Например, чтобы уйти «глубже» — пользователь из фрагмента «Главные настройки» переходит во фрагмент «Выбор языка». При этом новый фрагмент должен отобразиться на месте главного фрагмента. А потом пользователь еще и захочет вернуться обратно…

Дано

  • ViewPager2

  • список Fragment-ов (например, разделы анкеты или многостраничный раздел настроек)

  • Kotlin (Java), Android собственно

Задача

Необходимо заменять фрагменты динамически — на месте n-ого фрагмента должен появится другой. При этом все остальные страницы не должны терять своего состояния.

Пример:

Есть три фрагмента. Первый — список любимых тарелок, второй — экран с сообщениями, третий — настройки.

Пользователь на первом экране выбирает тарелку -> меняем первый фрагмент на фрагмент «Информация о тарелке»

Пользователь листает свайпами или выбирает через TabLayout третий экран — «Настройки», выбирает раздел «Выбор языка» -> меняем третий фрагмент на фрагмент «Выбор языка»

Теперь пользователь, не выходя из выбора языка, листает до первого фрагмента. Он должен увидеть информацию о тарелке, а не список любимых тарелок.

Итак, пытаемся сдружить ArrayMap и ViewPager2.

Результат

Будет такой

Дисклеймер (не читать, вода)

Пишу, потому что найти хороших решений по этой задаче не смог, хотя вроде пытался усердно.

Код местами специально упрощен — не люблю, когда в статьях добавляют десятки строк, не касающихся решения описываемой задачи, только ради соблюдения всех правил. Так что: строковые ресурсы — вынести, вью модели для фрагментов — добавить, пару уровней абстракций — ну вы поняли

Весь проект — https://github.com/IDolgopolov/ViewPager2_FragmentReplacer

Иногда буду использовать слово «страницы» — имею ввиду позицию, на которой отображается фрагмент во ViewPager. Например, «Список любимых тарелок», «Экран сообщений» и «Настройки» — три страницы. Фрагмент «Выбор языка» заменяет третью страницу.

Решение

Сначала ничего интересного, просто для общего ознакомления

Верстка

main_activity.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"     xmlns:tools="http://schemas.android.com/tools"     android:layout_width="match_parent"     android:layout_height="match_parent"     android:orientation="vertical"     >       <com.google.android.material.tabs.TabLayout         android:id="@+id/tab_layout"         android:layout_width="match_parent"         android:layout_height="wrap_content" />      <androidx.viewpager2.widget.ViewPager2         android:id="@+id/view_pager_2"         android:layout_width="match_parent"         android:layout_height="match_parent"         tools:context=".MainActivity"/>  </LinearLayout>
fragment_one.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout     xmlns:android="http://schemas.android.com/apk/res/android"     android:layout_width="match_parent"     android:layout_height="match_parent">      <Button         android:layout_width="match_parent"         android:layout_height="wrap_content"         android:text="Замени меня"         android:id="@+id/b_replace_fragment_one"         android:layout_centerInParent="true"         android:layout_marginHorizontal="20dp"/>  </RelativeLayout>
fragment_deep.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout     xmlns:android="http://schemas.android.com/apk/res/android"     android:layout_width="match_parent"     android:layout_height="match_parent">      <Button         android:layout_width="wrap_content"         android:layout_height="wrap_content"         android:text="Верни назад, блин"         android:id="@+id/b_back"         android:layout_margin="20dp"         />  </RelativeLayout>
fragment_two.xml, fragment_three.xml
<?xml version="1.0" encoding="utf-8"?> <TextView     android:layout_width="match_parent"     android:layout_height="match_parent"     android:text="Фрагмент 3"     android:gravity="center"     xmlns:android="http://schemas.android.com/apk/res/android" />

Интерфейсы

Опишем функции, которые нам понадобятся для смены фрагментов

interface FragmentReplacer {     fun replace(position: Int, newFragment: BaseFragment, isNotify: Boolean = true)     fun replaceDef(position: Int, isNotify: Boolean = true) : BaseFragment }

Все фрагменты, добавляемые во ViewPagerAdapter (MyViewPager2Adapter описан ниже), будут наследоваться от BaseFragment.

Определим в нем переменные для хранения:

  • pageId — уникальный номер страницы. Может быть любым числом, но главное — уникальным и большим, чем у вас позиций страниц (pageId > PAGE_COUNT — 1), иначе будут баги из-за метода getItemId()

  • pagePos — номер странице, на которой будет отображаться фрагмент во ViewPager (начиная с 0, естественно)

  • fragmentReplacer — ссылка на ViewPagerAdapter (MyViewPager2Adapter реализует FragmentReplacer)

abstract class BaseFragment(private val layoutId: Int) : Fragment() {     val pageId = Random.nextLong(2021, 2021*3)     var pagePos = -1     protected lateinit var fragmentReplacer: FragmentReplacer      override fun onCreateView(         inflater: LayoutInflater,         container: ViewGroup?,         savedInstanceState: Bundle?     ): View? {         return inflater.inflate(layoutId, container, false)     }      fun setPageInfo(pagePos: Int, fragmentReplacer: FragmentReplacer) {         this.pagePos = pagePos         this.fragmentReplacer = fragmentReplacer     } }

Связь активити и ViewPager2

class MainActivity : AppCompatActivity() {      override fun onCreate(savedInstanceState: Bundle?) {         super.onCreate(savedInstanceState)         setContentView(R.layout.activity_main)          //MyViewPager2Adapter - смотреть разбор далее         view_pager_2.adapter = MyViewPager2Adapter(this)         TabLayoutMediator(tab_layout, view_pager_2) { tab, position ->             tab.text = when(position) {                 0 -> "Первый"                 1 -> "Второй"                 2 -> "Крайний"                 else -> throw IllegalStateException()             }         }.attach()     } }

Адаптер для ViewPager2 — всё здесь

В этом классе будут реализован интерфейс FragmentReplacer и переопределены несколько классов FragmentStateAdapter. Это и позволит менять фрагменты на лету.

Весь код класса, который ниже разбираю подробно
class MyViewPager2Adapter(container: FragmentActivity) : FragmentStateAdapter(container),     FragmentReplacer {          companion object {         private const val PAGE_COUNT = 3     }      private val mapOfFragment = ArrayMap<Int, BaseFragment>()       override fun replace(position: Int, newFragment: BaseFragment, isNotify: Boolean) {         mapOfFragment[position] = newFragment         if (isNotify)             notifyItemChanged(position)     }      override fun replaceDef(position: Int, isNotify: Boolean): BaseFragment {         val fragment = when (position) {             0 -> FragmentOne()             1 -> FragmentTwo()             2 -> FragmentThree()             else -> throw IllegalStateException()         }         fragment.setPageInfo(             pagePos = position,             fragmentReplacer = this         )          replace(position, fragment, isNotify)          return fragment     }       override fun createFragment(position: Int): Fragment {         return mapOfFragment[position] ?: replaceDef(position, false)     }      override fun containsItem(itemId: Long): Boolean {         var isContains = false         mapOfFragment.values.forEach {             if (it.pageId == itemId) {                 isContains = true                 return@forEach             }         }         return isContains     }      override fun getItemId(position: Int) =         mapOfFragment[position]?.pageId ?: super.getItemId(position)      override fun getItemCount() = PAGE_COUNT }

В первую очередь переопределим четыре метода FragmentStateAdapter:

//ключ - позиция страницы //значения - фрагмент, отображаемый в данный момент на этой странице private val mapOfFragment = ArrayMap<Int, BaseFragment>()  //возвращаем количество страниц (в нашем примере - 3) override fun getItemCount() = PAGE_COUNT  /* 	эта функция вызывается, когда пользователь впервые открывает страницу 	или был вызван notifyItemChanged(position)      если фрагмент еще не был создан (mapOfFragment[position] == null),   то заменяем фрагмент на этой странице фрагментов по умолчанию */ override fun createFragment(position: Int): Fragment { 		return mapOfFragment[position] ?: replaceDef(position, false) }  /* 	у каждого нашего фрагмента есть свой id, отдаем его адаптеру 	если фрагмент на этой позиции еще не был создан, можно вернуть любую чипуху,  	не совпадающую со всеми существующими id   p.s. super.getItemId { return position } по умолчанию */ override fun getItemId(position: Int) = 		mapOfFragment[position]?.pageId ?: super.getItemId(position)   override fun containsItem(itemId: Long): Boolean {     var isContains = false        mapOfFragment.values.forEach { if (it.pageId == itemId) {            isContains = true            return@forEach        } }        return isContains } 

Теперь две функции, которые позволяют подменять фрагменты

override fun replace(position: Int, newFragment: BaseFragment, isNotify: Boolean) {     //указываем фрагменту, на какой странице он отображается     newFragment.setPageInfo(             pagePos = position,             fragmentReplacer = this         )          mapOfFragment[position] = newFragment   	//isNotify = true всегда, кроме вызова replace() из функции createFragment()   	//иначе приложение будет крашится, так как еще ничего не отображено     if (isNotify)     		notifyItemChanged(position) }  //здесь указываем базовые фрагменты - фрагменты, отображаемые по умолчанию //пригодится, когда захотим отобразить страницы в первый раз //или вернуться к дефолтной странице после того, как закончим все дела на замененной override fun replaceDef(position: Int, isNotify: Boolean): BaseFragment {     val fragment = when (position) {             0 -> FragmentOne()             1 -> FragmentTwo()             2 -> FragmentThree()             else -> throw IllegalStateException()     }      replace(position, fragment, isNotify)     return fragment }

Пример использования

Все готово — у каждого фрагмента есть ссылка на адаптер, значит есть возможность заменить фрагмент на любой странице, а самое главное (наиболее используемое на практике) — есть возмжность заменить самого себя.

Например, фрагмент, по умолчанию, отображаемый на первой странице:

FragmentOne.kt

class FragmentOne : BaseFragment(R.layout.fragment_one) {      override fun onViewCreated(view: View, savedInstanceState: Bundle?) {         super.onViewCreated(view, savedInstanceState) 				         //по нажатию кнопки         b_replace_fragment_one.setOnClickListener {           	//создаем фрагмент, который заменит этот фрагмент             val fragmentDeep = FragmentDeep() 					           	//заменяем              fragmentReplacer.replace(this.pagePos, fragmentDeep)         }     } }

FragmentDeep.kt

class FragmentDeep : BaseFragment(R.layout.fragment_deep) {  		override fun onViewCreated(view: View, savedInstanceState: Bundle?) {         super.onViewCreated(view, savedInstanceState)          b_back.setOnClickListener {           	//по нажатию кнопки заменяем текущий фрагмент            	//на дефолтный: 0 -> FragmentOne()             fragmentReplacer.replaceCurrentToDef()                          //или             //fragmentReplacer.replace(pagePos, FragmentOne())                          //или             //fragmentReplacer.replace(0, FragmentOne())         }     } }

Мысли в слух

Аккуратно, если фрагменты тяжелые, — надо задуматься об очищении mapOfFragment.

Возможно, стоит хранить Class<BaseFragment> вместо BaseFragment. Но тогда придется инициализировать их каждый раз в createFragment(). Меньше памяти, больше времени. Что думаете?

На правах…

dendolg.ru — портфолио, контакты для сотрудничества (после переезда на GithubPages браузер иногда банит https протокол, а иногда не банит — разбираюсь)

t.me/dolgopolovd — блог с нулем подписчиков, в котором рассуждаю о мобильном и промышленном дизайне и иногда отвлекаюсь от программирования

ссылка на оригинал статьи https://habr.com/ru/post/544680/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *