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()
}
}
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()
}
}