UserVisibleHint is false on selected ViewPager Fragment managed by FragmentStatePagerAdapter
Asked Answered
P

2

6

I've encountered a really hard to diagnose issue in an Android app. getUserVisibleHint() returns false on the currently selected fragment in a ViewPager when it should return true (because it is visible and selected).

I've characterized the instances I see this behavior as follows:

  • Fragment is selected and currently displayed in a ViewPager
  • ViewPager is managed by a FragmentStatePagerAdapter
  • Fragment was previously selected, its state was saved and later restored by the PagerAdapter
    • minimum of 3 tabs in the viewpager
    • user navigates to tab 3, then to tab 1 then back to tab 3.
  • App uses Support Library version 24.0.0 or greater
Pratfall answered 18/5, 2017 at 15:49 Comment(1)
Bug ReportPratfall
P
8

Debugging revealed that FragmentStatePagerAdapter is actually setting the state of the selected tab properly in setPrimaryItem(ViewGroup container, int position, Object object) but that it is later set to false in FragmentManager#moveToState(Fragment f, int newState, int transit, int transitionStyle, boolean keepActive)

//from FragmentManager#moveToState(Fragment f, int newState, int transit, int transitionStyle, boolean keepActive)
f.mUserVisibleHint = f.mSavedFragmentState.getBoolean(
        FragmentManagerImpl.USER_VISIBLE_HINT_TAG, true);

f.mSavedFragmentState above has saved the visible state as false because it was saved when the fragment was no longer on the screen.

So the issue here is state loss; the visible state is being set in FragmentStatePagerAdapter#setPrimaryItem but is lost some time before the fragment's onResume method is called.

The Fix

Until this bug is fixed in the library, override setPrimaryItem in your PagerAdapter and force any pending transactions to commit first.

public static class SectionsPagerAdapter extends FragmentStatePagerAdapter {
    public SectionsPagerAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public void setPrimaryItem(ViewGroup container, int position, Object object) {
        //Force any pending transactions to save before we set an item as primary
        finishUpdate(null);
        super.setPrimaryItem(container, position, object);
    }

    @Override
    public Fragment getItem(int position) {
        Fragment fragment = new DummySectionFragment();
        Bundle args = new Bundle();
        args.putInt(DummySectionFragment.ARG_SECTION_NUMBER, position + 1);
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public int getCount() {
        return 4;
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return "Page " + (position + 1);
    }
}

To fix this, FragmentStatePagerAdapter must commit any fragment transactions before setting the user visible hint.

FragmentStatePagerAdapter

Just to show what's happening inside FragmentStatePagerAdapter

@Override
public Object instantiateItem(ViewGroup container, int position) {
    // If we already have this item instantiated, there is nothing
    // to do.  This can happen when we are restoring the entire pager
    // from its saved state, where the fragment manager has already
    // taken care of restoring the fragments we previously had instantiated.
    if (mFragments.size() > position) {
        Fragment f = mFragments.get(position);
        if (f != null) {
            return f;
        }
    }

    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }

    Fragment fragment = getItem(position);
    if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
    if (mSavedState.size() > position) {
        Fragment.SavedState fss = mSavedState.get(position);
        if (fss != null) {
            fragment.setInitialSavedState(fss);
        }
    }
    while (mFragments.size() <= position) {
        mFragments.add(null);
    }
    fragment.setMenuVisibility(false);
    fragment.setUserVisibleHint(false);
    mFragments.set(position, fragment);
    mCurTransaction.add(container.getId(), fragment);

    return fragment;
}

@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
    Fragment fragment = (Fragment)object;
    if (fragment != mCurrentPrimaryItem) {
        if (mCurrentPrimaryItem != null) {
            mCurrentPrimaryItem.setMenuVisibility(false);
            mCurrentPrimaryItem.setUserVisibleHint(false);
        }
        if (fragment != null) {
            fragment.setMenuVisibility(true);
            fragment.setUserVisibleHint(true);
        }
        mCurrentPrimaryItem = fragment;
    }
}

@Override
public void finishUpdate(ViewGroup container) {
    if (mCurTransaction != null) {
        mCurTransaction.commitNowAllowingStateLoss();
        mCurTransaction = null;
    }
}
Pratfall answered 18/5, 2017 at 15:49 Comment(3)
Got the same bug with appcompat-v7:25.3.1. Your fix works, thanks.Carrico
I have the new version (27.0.2) after being marked as fixed by the google team, but the problem still happening and I tried your fix and still happening..! any help..?Crybaby
@AlaaAbuZarifa 27.0.2 was released in November 2017. The bug report marked this issue fixed in late February 2018. I would expect that it is fixed in 27.1.1 but I have not tested this version.Pratfall
P
1

If it is compatible with your project, try version 27.1.1 (or newer) of the support library. The related bug report was marked fixed in late February 2018 and version 27.1.1 was released in April 2018.

Pratfall answered 19/4, 2018 at 13:2 Comment(2)
I am using support lib version 27.1.1 and still getting this issue, will try your solution and update accordingly.Tinder
Your solution fixes this issue, however going through the code I see that FragmentStatePagerAdapter V13 has been deprecated since 27.1.0 and instead suggests to use FragmentStatePagerAdapter V4 which seems to work only with V4 support Fragments. Is this the dev's fix? Use V4 Fragments instead.Tinder

© 2022 - 2024 — McMap. All rights reserved.