Search code examples
androidkotlinandroid-fragmentstabsandroid-viewpager2

How to add/remove tabs in ViewPager2 at runtime? Using only one fragment


In the app I'm testing this concept, the app starts with one tab and the user can click a button inside the fragment and add a new tab with the name of an item inside a pre-defined list of movie titles. Imagine the list has 100 titles, how can I re-use the same fragment for each tab created so I don't have to make 100 fragments (one for each title)?

This link shows how to add/remove tabs using several fragments and a sample app.

class DynamicViewPagerAdapter(
    fragmentActivity: FragmentActivity,
    private val titleId: Int
) : FragmentStateAdapter(fragmentActivity) {

override fun createFragment(position: Int): Fragment {
    return DynamicFragment.getInstance(titleId)
}

override fun getItemCount(): Int {
    return titles.size
}

override fun getItemId(position: Int): Long {
    
    return position.toLong()
}

override fun containsItem(itemId: Long): Boolean {
    return titles.contains(titles[itemId.toInt()])
}

fun addTab(title: String) {
    titles.add(title)
    notifyDataSetChanged()
}

fun addTab(index: Int, title: String) {
    titles.add(index, title)
    notifyDataSetChanged()
}

fun removeTab(name: String) {
    titles.remove(name)
    notifyDataSetChanged()
}

fun removeTab(index: Int) {
    titles.removeAt(index)
    notifyDataSetChanged()
}

}


Solution

  • I figured it out, a very important part was to keep ordinals. In the process of removing tabs, the adapter makes multiple calls to getItemId() and containsItem(). In this process the adapter uses the position of the tab and the ordinal of the item in the tab. In the example I found which uses different fragments and a predefined number of them, they use enum to get the ordinals, while I used a MutableMap (titles and keys; ordinals as values). Here's the link to the finished test app.

    I used these global variable (only as a test):

    // pre-defined list of titles
    val testMovieTitles = listOf(
        "Hulk", "Chucky", "Cruella", "Nobody", "Scar Face",
        "Avatar", "Joker", "Profile", "Saw", "Ladies")
    
    val titles = mutableListOf("All Movies")
    val titlesOrdinals: MutableMap<String, Int> = mutableMapOf("All Movies" to 0)
    
    const val MY_LOG = "MY_LOG"
    

    The activity:

    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    import android.util.Log
    import com.example.testmyviewpager2.*
    import com.example.testmyviewpager2.databinding.ActivityDynamicViewPagerBinding
    import com.google.android.material.tabs.TabLayoutMediator
    
    class DynamicViewPagerActivity : AppCompatActivity() {
    
    private var binding: ActivityDynamicViewPagerBinding? = null
    var titleIncrementer = 0 // to use the next tile until it doesn't match one of the tabs
    
    val activityViewPagerAdapter: DynamicViewPagerAdapter by lazy {
        DynamicViewPagerAdapter(this)}
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityDynamicViewPagerBinding.inflate(layoutInflater)
        setContentView(binding!!.root)
    
        setUpTabs()
        addTabFabOnClick()
    }
    
    private fun setUpTabs() {
        binding!!.dynamicViewPager.offscreenPageLimit = 4
        binding!!.dynamicViewPager.adapter = activityViewPagerAdapter
    
        // Set the title of the tabs
        TabLayoutMediator(binding!!.dynamicTabLayout, binding!!.dynamicViewPager) { tab, position ->
            tab.text = titles[position]
        }.attach()
    }
    
    private fun addTabFabOnClick() {
        binding!!.addTabFab.setOnClickListener {
            val nextTitlePosition = titles.size - 1
            val nextOrdinalId = titlesOrdinals.size - 1
            var nextTitle = testMovieTitles[nextTitlePosition]
    
            // if a title has been added before, don't add it
            // new tabs cannot have the same name as old tabs
            while(titles.contains(nextTitle)) {
                titleIncrementer++
                nextTitle = testMovieTitles[nextTitlePosition + titleIncrementer]
            }
            if (titleIncrementer > 0) { Log.d("${MY_LOG}Activity", "incrementer: $titleIncrementer") }
    
            if(!titles.contains(nextTitle)) {
                activityViewPagerAdapter.addTab(nextOrdinalId+1, nextTitle)
            } else {
                Log.d("${MY_LOG}Activity", "\t\t titles contains next title \t\t $titles $nextTitle")}
        }
    }
    }
    

    The re-usable fragment:

    import android.os.Bundle
    import android.util.Log
    import androidx.fragment.app.Fragment
    import android.view.LayoutInflater
    import android.view.View
    import android.view.ViewGroup
    import com.example.testmyviewpager2.*
    import kotlinx.android.synthetic.main.fragment_dynamic.*
    
    class DynamicFragment : Fragment() {
    
    private var fragmentViewPagerAdapter: DynamicViewPagerAdapter? = null
    private var titleToDisplay = "All Movies"
    
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_dynamic, container, false)
    }
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        // get the adapter instance from the main activity
        fragmentViewPagerAdapter = (activity as? DynamicViewPagerActivity)!!.activityViewPagerAdapter
        removeButtonOnClick()
        dynamic_fragment_text.text = titleToDisplay
        Log.d("${MY_LOG}fragCreated", "name: ${titleToDisplay}")
        super.onViewCreated(view, savedInstanceState)
    }
    
    override fun onDestroy() {
        Log.d("${MY_LOG}destroyed", "\t\t\t $titles")
        Log.d("${MY_LOG}destroyed", "\t\t\t $titlesOrdinals")
        super.onDestroy()
    }
    
    private fun removeButtonOnClick() {
        removeButton.setOnClickListener {
    
            val numOfTabs = titles.size
            if (numOfTabs > 1 && titleToDisplay != "All Movies") {
                fragmentViewPagerAdapter!!.removeTab(titleToDisplay)
            }
        }
    }
    
    fun setTitleText(title: String) {
        titleToDisplay = title
    }
    
    companion object{
        //The Fragment retrieves the Item from the List and display the content of that item.
        fun getInstance(titleId: Int): DynamicFragment {
            val thisDynamicFragment = DynamicFragment()
            val titleToDisplay = titles[titleId]
            thisDynamicFragment.setTitleText(titleToDisplay)
            return thisDynamicFragment
        }
    }
    }
    

    The ViewPager2 Adapter:

    import android.util.Log
    import androidx.fragment.app.Fragment
    import androidx.fragment.app.FragmentActivity
    import androidx.viewpager2.adapter.FragmentStateAdapter
    import com.example.testmyviewpager2.*
    
    class DynamicViewPagerAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) {
    
    private val theActivity = DynamicViewPagerActivity()
    
    override fun createFragment(position: Int): Fragment {
        // Used this to change the text inside each fragment
        return DynamicFragment.getInstance(titles.size-1)
    }
    
    override fun getItemCount(): Int {
        return titles.size
    }
    
    override fun getItemId(position: Int): Long {
        return titlesOrdinals[titles[position]]!!.toLong()
    }
    
    // called when a tab is removed
    override fun containsItem(itemId: Long): Boolean {
        var thisTitle = "No Title"
        titlesOrdinals.forEach{ (k, v) ->
            if(v == itemId.toInt()) {
                thisTitle = k
            }
        }
        return titles.contains(thisTitle)
    }
    
    fun addTab(ordinal: Int, title: String) {
        titles.add(title)
    
        // don't rewrite an ordinal
        if(!titlesOrdinals.containsKey(title)) {
            titlesOrdinals[title] = ordinal
        }
        notifyDataSetChanged()
        Log.d("${MY_LOG}created", "\t\t\t $titles")
        Log.d("${MY_LOG}created", "\t\t\t $titlesOrdinals")
    }
    
    fun removeTab(name: String) {
        titles.remove(name)
        notifyDataSetChanged()
        Log.d("${MY_LOG}removeTab", "----------------")
    }
    
    fun removeTab(index: Int) {
        titles.removeAt(index)
        notifyDataSetChanged()
    }
    }