Search code examples
androidandroid-fragmentsandroid-tablayoutandroid-viewpager2

How do I dynamically add and remove fragment pages using Viewpager2 and Mediator Tab Layout


I need to add fragments to viewpager2 as well as tabs at runtime depending on server config file. Most viewpager2 resources I have seen showcase a static viewpager2 using the new TabMediator Tab layout. I am skipping viewpager as I need to add RTL and vertical scrolling supported by viewpager2.

My biggest concern is how to I track the index of a specific page/fragment when I add and remove fragments. Have gone through this tutorial and code as an exapmple.


Solution

  • This is how you implement it in Kotlin

    viewPager = binding.viewPagerContainer
            val tabLayout : TabLayout = binding.tabLayout
    
            val fragmentList : MutableList<Pair<String, Fragment>> = mutableListOf()
            fragmentList.add(Pair(getString(R.string.assets), AssetFragment.newInstance()))
            fragmentList.add(Pair(getString(R.string.news), NewsFragment.newInstance()))
    
            val adapter = AppFragmentAdapter(fragmentList, this)
    
            Handler(Looper.myLooper()!!).postDelayed({
                adapter.addFragment(Pair(getString(R.string.videos), VideosFragment.newInstance()))
            }, 1000)
    
            Handler(Looper.myLooper()!!).postDelayed({
                adapter.removeFragment(2)
            }, 2000)
    
            viewPager.adapter = adapter
            viewPager.offscreenPageLimit = 2
    
            val layoutInflater : LayoutInflater = LayoutInflater.from(context)
    
            viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
    
                override fun onPageSelected(position: Int) {
                    super.onPageSelected(position)
                    val actionBar : ActionBar = (requireActivity() as AppCompatActivity).supportActionBar!!
                    actionBar.title = adapter.getFragmentName(position)
                }
    
            })
    
            TabLayoutMediator(tabLayout, viewPager){ tab, position ->
                tab.customView = prepareTabView(layoutInflater, tabLayout, adapter.getFragmentName(position), tabIcons[position])
            }.attach()
    

    Here we already tested to add and remove the last fragment using some basic delay upon launching an Activity or Parent Fragment. You can create a custom view for each tab or just use tab.text = "Tab Name" for simplicity.

    Now the type of adapter you will be using with ViewPager2 for Fragment is FragmentStateAdapter

    class AppFragmentAdapter(private val fragmentList: MutableList<Pair<String, Fragment>>, fragment: Fragment) : FragmentStateAdapter(fragment) {
    
    //    private var pageIds = fragmentList.map { fragmentList.hashCode().toLong() }
    
        override fun getItemCount(): Int = fragmentList.size
    
        override fun createFragment(position: Int): Fragment {
            return fragmentList[position].second
        }
    
    //    override fun getItemId(position: Int): Long = pageIds[position] // Make sure notifyDataSetChanged() works
    
    //    override fun containsItem(itemId: Long): Boolean = pageIds.contains(itemId)
    
        fun getFragmentName(position: Int) = fragmentList[position].first
    
        fun addFragment(fragment: Pair<String, Fragment>) {
            fragmentList.add(fragment)
            notifyDataSetChanged()
        }
    
        fun removeFragment(position: Int) {
            fragmentList.removeAt(position)
            notifyDataSetChanged()
        }
    
    }
    

    If your structure is Parent Fragment > ViewPager2 > Child Fragments the above code will work fine.

    If your structure is Activity > ViewPager2 > Fragments just change

    class AppFragmentAdapter(private val fragmentList: MutableList<Pair<String, Fragment>>, fragment: Fragment) : FragmentStateAdapter(fragment)
    

    to

    class AppFragmentAdapter(private val fragmentList: MutableList<Pair<String, Fragment>>, fragment: FragmentActivity) : FragmentStateAdapter(fragment)
    

    then instead of

    val adapter = AppFragmentAdapter(fragmentList, this)
    

    pass the Fragment's activity like this

    val adapter = AppFragmentAdapter(fragmentList, requireActivity())
    

    Internally FragmentStateAdapter(fragment) from our adapter already handle which FragmentManager is supposedly use.

    Activity > ViewPager2 > Fragments

    public FragmentStateAdapter(@NonNull FragmentActivity fragmentActivity) {
        this(fragmentActivity.getSupportFragmentManager(), fragmentActivity.getLifecycle());
    }
    

    Parent Fragment > ViewPager2 > Child Fragments

    public FragmentStateAdapter(@NonNull Fragment fragment) {
        this(fragment.getChildFragmentManager(), fragment.getLifecycle());
    }
    

    I was deceived first by searching in SO about dynamically add/remove Fragment with ViewPager2 before trying the simplest approach I could come up.