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
Jon
  • 9,156
  • 9
  • 56
  • 73

2 Answers2

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;
    }
}
Community
  • 1
  • 1
Jon
  • 9,156
  • 9
  • 56
  • 73
  • Got the same bug with appcompat-v7:25.3.1. Your fix works, thanks. – alders Aug 30 '17 at 11:41
  • 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..? – Alaa AbuZarifa Apr 19 '18 at 09:33
  • @AlaaAbuZarifa `27.0.2` was released in November 2017. The [bug report](https://issuetracker.google.com/issues/38414371) 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. – Jon Apr 19 '18 at 12:55
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.

Jon
  • 9,156
  • 9
  • 56
  • 73
  • I am using support lib version `27.1.1` and still getting this issue, will try your solution and update accordingly. – Abbas Jul 13 '18 at 06:33
  • Your solution fixes this issue, however going through the code I see that [`FragmentStatePagerAdapter` V13](https://developer.android.com/reference/android/support/v13/app/FragmentStatePagerAdapter) has been deprecated since `27.1.0` and instead suggests to use [`FragmentStatePagerAdapter` V4](https://developer.android.com/reference/android/support/v4/app/FragmentPagerAdapter) which seems to work only with V4 support Fragments. Is *this* the dev's fix? Use V4 Fragments instead. – Abbas Jul 13 '18 at 07:04