NotifyDataSetChanged not working in FragmentStateAdapter with Viewpager2

8.2k views Asked by At

I am making a file selection page with viewpager2 with FragmentStateAdapter. In one of the pages I show all mounted storage devices on which I would like to tap to view the contents but I want the contents to be on another fragment.

I want to replace fragment with another one in the last page of viewpager. but with the following code, the page just refreshes and new Fragment is not created as createFragment does not get called.

My PagerAdapter

public static class PagerAdapter extends FragmentStateAdapter {


        private Fragment mFragmentAtPos0;
        private final FragmentManager mFragmentManager;
        private final FirstPageListener listener = new FirstPageListener();
        public final class FirstPageListener implements
                FirstPageFragmentListener {
            public void onSwitchToNextFragment() {
                mFragmentManager.beginTransaction().remove(mFragmentAtPos0)
                        .commit();
                if (mFragmentAtPos0 instanceof FilesandFolder_Others_MainPage){
                    mFragmentAtPos0 = new FileExplorer(listener);
                }else{ // Instance of NextFragment
                    mFragmentAtPos0 = new FilesandFolder_Others_MainPage(listener);
                }
                notifyDataSetChanged();
            }
            public void onSwitchToNextFragment(Bundle bundle) {
//                mFragmentManager.beginTransaction().remove(mFragmentAtPos0)
//                        .commit();
                if (mFragmentAtPos0 instanceof FilesandFolder_Others_MainPage){
                    mFragmentAtPos0 = new FileExplorer(listener);
                    mFragmentAtPos0.setArguments(bundle);
                }else{ // Instance of NextFragment
                    mFragmentAtPos0 = new FilesandFolder_Others_MainPage(listener);
                    mFragmentAtPos0.setArguments(bundle);
                }
                notifyDataSetChanged();
//                notifyItemChanged(getItemPosition(mFragmentAtPos0));
            }
        }


//        @Override
//        public void onBindViewHolder(@NonNull FragmentViewHolder holder, int position, @NonNull List<Object> payloads) {
//            super.onBindViewHolder(holder, position, payloads);
//            if (position == getItemPosition(mFragmentAtPos0)) {
//
//                Fragment f = mFragmentManager.findFragmentByTag("f" + holder.getItemId());
//                if (f != null) {
//                    mFragmentManager.beginTransaction().replace(holder.getItemId(), )
//                }
//            }
//        }

        private int getItemPosition(Fragment mFragmentAtPos0) {
            for (int i = 0; i < getItemCount(); i++){

                if (createFragment(i).equals(mFragmentAtPos0)){
                    return i;
                }
            }
            return -1;
        }


        public PagerAdapter(FragmentActivity fm) {
            super(fm);
            mFragmentManager = fm.getSupportFragmentManager();
            mFragmentAtPos0 = new FilesandFolder_Others_MainPage(listener);
        }

        @NonNull
        @Override
        public Fragment createFragment(int position) {
            switch (position){
                case 0:
                    return new AppSelectionFragment();
                case 1:
                    return new Photos();
                case 2:
                    return new VideoGalleryFragment();
                case 3:
                    return new Gallery();
                default:
                    return mFragmentAtPos0;
            }
        }

        @Override
        public int getItemCount() {
            return 5;
        }
    }

Here is the constructor of Fragment from which I would like to replace to another Fragment:

    private static FileSelection.PagerAdapter.FirstPageListener pageFragmentListener;

    public FilesandFolder_Others_MainPage() {
    }
    public FilesandFolder_Others_MainPage(FileSelection.PagerAdapter.FirstPageListener firstPageFragmentListener) {
        pageFragmentListener = firstPageFragmentListener;
    }

And on tap of a view, the following code is supposed to be called:-

Bundle bundle = new Bundle();
bundle.putString("PATH", volumes.get(position).path);
pageFragmentListener.onSwitchToNextFragment(bundle);

I have searched all over stackoverflow, and never saw a proper answer with viewpager 2 and FragmentStateAdapter as everyone seems to be using Viewpager

I tried to implement this:- FragmentStateAdapter for ViewPager2 notifyItemChanged not working as expected

but couldn't understand how to edit my code. Pls help Thanks in advance

3

There are 3 answers

0
sebschaef On

I faced the same issue today and thought I would share my solution here. Instead of intercepting onBindViewHolder(), my solution simply replaces the old fragment:

OldFragment.instance.containerId?.let {
    replace(it, NewFragment.instance)
}

To be more concrete:

My App has a ViewPager2 with two fragments. Let's call them MainFragment and DetailFragment. On a button click in the MainFragment, I wanted to replace it with a SecondMainFragment. And the same behaviour vice versa: Button click in SecondMainFragment would replace itself with the MainFragment.

class CustomFragmentStateAdapter(
    private val fragmentActivity: FragmentActivity
) : FragmentStateAdapter(fragmentActivity) {

    var displaySecondMainFragment: Boolean = false
        set(value) {
            if (field != value) {
                switchFragments(replaceWithSecondMainFragment = value)
            }
            field = value
        }

    private fun switchFragments(replaceWithSecondMainFragment: Boolean) {
        fragmentActivity.supportFragmentManager.commit {
            if (replaceWithSecondMainFragment) {
                MainFragment.instance.containerId?.let {
                    replace(it, SecondMainFragment.instance)
                }
            } else {
                SecondMainFragment.instance.containerId?.let {
                    replace(it, MainFragment.instance)
                }
            }
        }
    }
        
    // Standard adapter overrides
    override fun getItemCount(): Int = 2

    override fun createFragment(position: Int): Fragment = when (position) {
        0 -> if (displaySecondMainFragment) SecondMainFragment.instance else MainFragment.instance
        else -> DetailFragment.instance
    } 
}

With this extension property to get the id of the container the fragment is in:

inline val Fragment.containerId: Int?
    get() = (view?.parent as? ViewGroup?)?.id

In the end I would only need to do something like that:

button.setOnClickListener {
    customFragmentStateAdapter.displaySecondMainFragment = true // Or false for the other way
}
1
mengxn On

you can override this two method

    override fun getItemId(position: Int): Long {
        // generate new id
        return getItem(position).hashCode().toLong()
    }

    override fun containsItem(itemId: Long): Boolean {
        // false if item is changed
        return dataList.find { it.hashCode().toLong() == itemId } != null
    }
3
Vitalii Husak On

Had same problem, and that's what i found:

FragmentStateAdapter has method onBindViewHolder(holder, position, payloads), which you can override (unlike onBindViewHolder(holder, position) which is final). This method calls when you call notify methods, and for notifyItemChanged(), notifyItemRangeChanged() methods you can access the fragment, and manually update it.

Here is code to access the fragment:

public void onBindViewHolder(@NonNull FragmentViewHolder holder, int position, @NonNull List<Object> payloads) {

    String tag = "f" + holder.getItemId();

    Fragment fragment = fragmentManager.findFragmentByTag(tag);

    if (fragment != null) {
        //manual update fragment
    } else {
        // fragment might be null, if it`s call of notifyDatasetChanged() 
        // which is updates whole list, not specific fragment
        super.onBindViewHolder(holder, position, payloads);
    }
}

FragmentStateAdapter is tagged his fragments by "f" + holder.getItemId(), that's why it works.

Also you can use DiffUtil, and this is better way. Here is good example of solving this problem, and some links in the end for better understanding of ViewPager2.