I am from ViewPager with FragmentPagerAdapter which has a very straight forward implementation like this.
public class FragmentAdapters extends FragmentPagerAdapter { private final List<Fragment> mFragmentList = new ArrayList<>(); private final List<String> mFragmentTitleList = new ArrayList<>(); public FragmentAdapters(@NonNull FragmentManager fm, int behavior) { super(fm, behavior); } public void addFragment(Fragment fragment, String title) { mFragmentList.add(fragment); mFragmentTitleList.add(title); } @Override public CharSequence getPageTitle(int position) { return mFragmentTitleList.get(position); } @NonNull @Override public Fragment getItem(int position) { return mFragmentList.get(position); } @Override public int getCount() { return mFragmentList.size(); } }
Parent Fragment
adapter = new FragmentAdapters(getChildFragmentManager(), FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); CoinsFragment coinsFragment = new AssetFragment(); NewsFragment newsFragment = new NewsFragment(); VideoFragment videoFragment = new VideoFragment(); adapter.addFragment(coinsFragment, getString(R.string.asset)); adapter.addFragment(newsFragment, getString(R.string.news)); adapter.addFragment(videoFragment, getString(R.string.videos)); mViewPager.setAdapter(adapter); mViewPager.setOffscreenPageLimit(2);
Android recommends moving away from ViewPager and deprecating the beloved FragmentPagerAdapter, now I am trying to work with ViewPager2 with FragmentStateAdapter but having a lot of confusion and difficulty in terms of how it works.
These is my implementation
class AppFragmentAdapter(private val fragmentList: MutableList<Pair<String, Fragment>>, fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) { private val pageIds = fragmentList.map { fragmentList.hashCode().toLong() } override fun getItemCount(): Int = fragmentList.size override fun createFragment(position: Int): Fragment = when (position) { 0 -> { Log.wtf("WTF", fragmentList[position].first + " " + position) fragmentList[position].second } 1 -> { Log.wtf("WTF", fragmentList[position].first + " " + position) fragmentList[position].second } 2 -> { Log.wtf("WTF", fragmentList[position].first + " " + position) fragmentList[position].second } else -> throw IllegalStateException("Invalid adapter position") } fun getFragmentName(position: Int) = fragmentList[position].first // fun addFragment(fragment: Pair<String, Fragment>) { // fragmentList.add(fragment) // notifyItemInserted(fragmentList.size) // notifyItemRangeChanged(fragmentList.size, fragmentList.size) // notifyDataSetChanged() // } // // fun removeFragment(position: Int) { // fragmentList.removeAt(position) // notifyItemRemoved(position) // notifyItemRangeChanged(fragmentList.size, fragmentList.size) // notifyDataSetChanged() // } override fun getItemId(position: Int): Long { return pageIds[position] // Make sure notifyDataSetChanged() works } override fun containsItem(itemId: Long): Boolean { return pageIds.contains(itemId) } }
Normally we could just return immediately the fragment from the list in createFragment
method but there is a huge confusion so I tried to expand it to see what is happening. What surprised me is when createFragment
is getting called one (1) time and not by how many fragments is available return by getItemCount
thus only one fragment is being shown and to make it even more confusing my supposedly first fragment (AssetFragment) was put at the very last position and the first two fragment is empty screen in which I do not know where that two fragment even came from.
Parent Fragment
val fragmentList : MutableList<Pair<String,Fragment>> = ArrayList() fragmentList.add(Pair(getString(R.string.assets), AssetFragment.newInstance())) fragmentList.add(Pair(getString(R.string.news), NewsFragment.newInstance())) fragmentList.add(Pair(getString(R.string.videos), VideosFragment.newInstance())) val adapter = AppFragmentAdapter(fragmentList, requireActivity()) viewPager.adapter = adapter viewPager.offscreenPageLimit = 2
This is what my Fragment looks like which basically nothing special
class AssetFragment : BaseFragment() { companion object { fun newInstance() = AssetFragment() } private lateinit var viewModel: AssetViewModel override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return inflater.inflate(R.layout.asset_fragment, container, false) } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) viewModel = ViewModelProvider(this).get(AssetViewModel::class.java) // TODO: Use the ViewModel } }
May someone enlighten me what is this ridiculous behavior?
P.S. I tried to make some log with getItemCount
and it was being called 23 times just for a 3 Fragment?
Advertisement
Answer
This is weird as overriding
getItemId() containsItem()
will just give this undesirable behavior when using different kinds of Fragments class I have.
In the end all I need was a simple FragmentStateAdapter
class like this
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() } }
Too bad I search for an immediate answer on how to make ViewPager2 dynamic without giving a shot first on the simplest approach I could come up. Many answer here on SO pointing out that getItemId()
and containsItem()
needs to be override when adding or removing Fragment(s) on ViewPager2 which gives some headache for almost 2 days. Felt betrayed.