Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NotifyDataSetChanged not working in FragmentStateAdapter with Viewpager2

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

like image 776
Saksham Gupta Avatar asked Jul 13 '20 17:07

Saksham Gupta


People also ask

What is the difference between ViewPager and ViewPager2?

ViewPager2 is an improved version of the ViewPager library that offers enhanced functionality and addresses common difficulties with using ViewPager . If your app already uses ViewPager , read this page to learn more about migrating to ViewPager2 .

How do I refresh Fragmentstateadapter?

Just refresh your HomeFragment or ProfileFragment by overriding onResume method. In onResume Method you can refresh your ListFragment or Fragment easily.

How do I update my Android ViewPager adapter?

The ViewPager and pager adapter just deal with data in memory. So when data in memory is updated, we just need to call the adapter's notifyDataSetChanged() . Since the fragment is already created, the adapter's onItemPosition() will be called before notifyDataSetChanged() returns.

What is ViewPager2 in Android?

So a Viewpager is an android widget that is used to navigate from one page to another page by swiping left or right using the same activity. So when we use Viewpager, we can add different layouts in one activity and this can be done by using the fragments. Famous applications like WhatsApp, Snapchat uses Viewpager.


3 Answers

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.

like image 88
Vitalii Husak Avatar answered Oct 16 '22 10:10

Vitalii Husak


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
    }
like image 23
mengxn Avatar answered Oct 16 '22 11:10

mengxn


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
}
like image 1
sebschaef Avatar answered Oct 16 '22 10:10

sebschaef