Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Fragment create/restore a duplicate view when re-attach

I have 6 pages in my ViewPager and OffscreenPageLimit=2(easy to reproduce my bug).
all data in the 6 pages are from server. In onCreateView, I send a request to server, and refresh UI when I get data from server.

When I select first tab and change to last quickly several times, some pagers display wrong. And at that time my field mMainLayout in fragment is not null.

For example, I have a ListView in my first page. When the page is wrong, another ListView is on the top of the right ListView. When I try to scroll ListView, only the right one(bottom one) moved.

I know that my response listener holds reference of mMainLayout and some other views, I create a new mMainLayout in method onCreateView, I thought fragment use new mMainLayout when it re-attach/restore and drop the old one(or remove it from the container). But I was wrong.

I know that FragmentPagerAdapter attach/re-attach fragment in instantiateItem and detach in destroyItem. Adapter didn't remove a fragment. Adapter didn't make a new fragment. Fragment keep the view and view states itself.

I remove mMainLayout from container and set all views in my fragment null at onDestroyView and save nothing in onSaveInstanceState. But still easy to reproduce the bug.

activity:

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    registerReceiver(receiver, new IntentFilter("com.souyidai.intent.action.log_fragment"));
    FragmentManager fragmentManager = getFragmentManager();
    mResources = getResources();
    mMainPagerAdapter = new MainPagerAdapter(fragmentManager);
    mPager = (ViewPager) findViewById(R.id.pager);
    mPager.setAdapter(mMainPagerAdapter);
    mPager.setOffscreenPageLimit(CACHE_SIZE);
    mIndicator = (TabPageIndicator) findViewById(R.id.indicator);
    mIndicator.setViewPager(mPager);
    ...
}

private class MainPagerAdapter extends FragmentPagerAdapter {
    public MainPagerAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public Fragment getItem(int position) {
        MainConfig.TabItem tab = mTabs.get(position);
        String tabType = tab.getTabType();
        String code = tab.getCode();
        MainConfig.TabItem.SubTabItem subTabItem = tab.getSubitem().get(0);
        Fragment fragment = MainFragment.newInstance(code, subTabItem);
        return fragment;
    }

    @Override
    public CharSequence getPageTitle(int position) {
        MainConfig.TabItem tab = mTabs.get(position);
        return tab.getTitle();
    }

    @Override
    public int getCount() {
        return mTabs.size();
    }
}

fragment:

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    mContainer = container;
    mMainLayout = inflater.inflate(R.layout.fragment_main, container, false);
    ...
}


@Override
public void onDestroyView() {
    super.onDestroyView();
    mContainer.removeView(mMainLayout);
    mContainer = null;
    mMainLayout = null;
    mSwipeRefreshLayout = null;
    mListView = null;
    mHeaderLayout = null;
    mFooterLayout = null;
    ...
}

android.support.v13.app.FragmentPagerAdapter

@Override
public Object instantiateItem(ViewGroup container, int position) {
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }

    final long itemId = getItemId(position);

    // Do we already have this fragment?
    String name = makeFragmentName(container.getId(), itemId);
    Fragment fragment = mFragmentManager.findFragmentByTag(name);
    if (fragment != null) {
        if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
        mCurTransaction.attach(fragment);
    } else {
        fragment = getItem(position);
        if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
        mCurTransaction.add(container.getId(), fragment,
                makeFragmentName(container.getId(), itemId));
    }
    if (fragment != mCurrentPrimaryItem) {
        FragmentCompat.setMenuVisibility(fragment, false);
        FragmentCompat.setUserVisibleHint(fragment, false);
    }

    return fragment;
}

I find this:

Android fragment onCreateView creating duplicate views on top of each other

But I only create new fragment in FragmentPagerAdapter.getItem. So we're different.

I solve this by removing all children of mMainLayout if mMainLayout is not null in method onCreateView. Then everything is fine. But I am still confused as to why this bug happens?

I did some tests.
1. try to add a fragment twice, app crash and log says: java.lang.IllegalStateException: Fragment already added: ...
2. try to attach a fragment twice, system does not call Fragment.onCreate or Fragment.onCreateView

like image 760
android_su Avatar asked Nov 23 '16 10:11

android_su


2 Answers

This situation occurred due to custom implementation of instantiateItem() One thing I have noticed. You are messing check if fragment is already added.

@Override
public Object instantiateItem(ViewGroup container, int position) {
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }

    final long itemId = getItemId(position);

    // Do we already have this fragment?
    String name = makeFragmentName(container.getId(), itemId);
    Fragment fragment = mFragmentManager.findFragmentByTag(name);
    if (fragment != null) {
        if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
        //make sure you are not attaching already added fragment
        //try with comment and uncomment two lines below
        //if(!fragment.isAdded())
        //mCurTransaction.attach(fragment);
    } else {
        fragment = getItem(position);
        if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
        mCurTransaction.add(container.getId(), fragment,
                makeFragmentName(container.getId(), itemId));
    }
    if (fragment != mCurrentPrimaryItem) {
        FragmentCompat.setMenuVisibility(fragment, false);
        FragmentCompat.setUserVisibleHint(fragment, false);
    }

    return fragment;
}

Don't know why you are creating class member field for mMainLayout

//try to go with default
View view = inflater.inflate(R.layout.fragment_main, container, false);

recommend you to study this

like image 84
Qamar Avatar answered Oct 25 '22 20:10

Qamar


You can try with code, I was facing similar issue got resolved with bellow changes. In that case no need to make null all the references in onDestroyView. as in that caseyou need to always make null check before using these reference anywhere else in the code.

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    if(mMainLayout == null)
    {
      mMainLayout = inflater.inflate(R.layout.fragment_main, container,false);
...
    } 
    return mMainLayout;
}

When mMainlayout is not null, it mean that your fragment instance has already one instance of the mMainLayout and already added to ViewGroup container no need to add it again. You are facing issue as you are adding same view again to same container.

like image 27
Swapnil Avatar answered Oct 25 '22 20:10

Swapnil