javaandroidkotlinandroid-viewpager2fragmentstateadapter

Issue on implementing ViewPager2 with FragmentStateAdapter


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?


Solution

  • 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.