使用ViewPager+FragmentAdapter 增删Fragment 异常及bug

来源:互联网 发布:初中生 18cm 知乎 编辑:程序博客网 时间:2024/04/30 13:51

使用ViewPager+FragmentAdapter 删除一Fragment,并notifyDataSetChanged().然而该移除的Fragment没有被移除,不该移除的反而被移除.

FragmentAdapter 子类如下


package com.sjj.echo.explorer;import android.support.v4.app.Fragment;import android.support.v4.app.FragmentManager;import android.support.v4.view.PagerAdapter;import android.support.v4.view.ViewPager;import com.sjj.echo.explorer.routine.FileTool;import com.sjj.echo.lib.FragmentStatePagerAdapterFix;import java.util.Iterator;import java.util.LinkedList;import java.util.List;/** * Created by SJJ on 2016/12/11. *//*don't extends FragmentPagerAdapter ! It will be in mess after delete a fragment */public class FilePageAdapter extends FragmentPagerAdapter {    MainActivity activity;    ViewPager viewPager;    List<FileFragment> dirs = new LinkedList<>();    String[] initDirs ;    public FilePageAdapter(FragmentManager fm, MainActivity activity, ViewPager viewPager) {        super(fm);        this.activity = activity;        this.viewPager = viewPager;        String[] _initDirs = {"/sdcard/","/","/data/","/cache/"};        initDirs = _initDirs;        int count = initDirs.length;        for(int i=0;i<count;i++)        {            FileFragment fileFragment =new FileFragment();            fileFragment.init(activity,initDirs[i],activity);            dirs.add(fileFragment);        }    }    public void addTab(String initPath)    {        FileFragment fileFragment = new FileFragment();        fileFragment.init(activity,initPath,activity);        dirs.add(fileFragment);        this.notifyDataSetChanged();        viewPager.setCurrentItem(dirs.size()-1);    }    public void removeTab(int index)    {        if(dirs.size()<=1)            return;        dirs.remove(index);        this.notifyDataSetChanged();    }    @Override    public CharSequence getPageTitle(int position) {        FileFragment fileFragment = (FileFragment)getItem(position);        FileListView fileListView = fileFragment.fileList;        String path = "/";        if(fileListView!=null)            path = fileListView.getCurPath();        else            path = fileFragment.launchDir;        //Log.d("@echo off","getPageTitle|position="+position+"|path="+path);        return FileTool.pathToName(path);    }    @Override    public Fragment getItem(int position) {        //Log.d("@echo off","getItem|pos="+position);        return dirs.get(position);    }    @Override    public int getCount() {       // Log.d("@echo off","getCount");        return dirs.size();    }}

经过一番google.

参考:

http://speakman.net.nz/blog/2014/02/20/a-bug-in-and-a-fix-for-the-way-fragmentstatepageradapter-handles-fragment-restoration/

解决方法:

继承自FragmentStatePagerAdapter 而不要继承 FragmentPagerAdapter 


------------------------------------------------------两天后更新---------------------------------------------------------------------------------


继承FragmentStatePagerAdapter 后任然存在问题.当删除一Fragment后来回滚动ViewPager,会出现java.lang.IllegalStateException: Fragment already active.

google后发现要重载public int getItemPosition(Object object).

@Override    public int getItemPosition(Object object) {        int index = dirs.indexOf(object);        if(index<0)            return PagerAdapter.POSITION_NONE;        return index;    }

删除Fragment后没有异常,但是滚动ViewPager后下一页为空白.

后来发现这篇文章:http://billynyh.github.io/blog/2014/03/02/fragment-state-pager-adapter/

发现是FragmentStatePagerAdapte的一个bug,给出了两个解决方法:

1.在getItemPosition中总是返回PagerAdapter.POSITON_NONE

2.copy FragmentStatePagerAdapte源码并修改 public void finishUpdate(ViewGroup container)为:

@Override    public void finishUpdate(ViewGroup container) {        if (mCurTransaction != null) {            mCurTransaction.commitAllowingStateLoss();            mCurTransaction = null;            mFragmentManager.executePendingTransactions();        }        ArrayList<Fragment> update = new ArrayList<Fragment>();        for (int i=0, n=mFragments.size(); i < n; i++) {            Fragment f = mFragments.get(i);            if (f == null) continue;            int pos = getItemPosition(f);            while (update.size() <= pos) {                update.add(null);            }            update.set(pos, f);        }        mFragments = update;    }
http://download.csdn.net/detail/outofmemo/9714438
考虑到天朝网络,将原文粘贴如下:

------------------------------------------------------------------------------------------------------------------------------------------------------------------------

A Deeper Look of ViewPager and FragmentStatePagerAdaper

Background

Last week I was working on a remove action on ViewPager, so based on the knowledge from ListView’s adapter, I tried removing the item and called notifyDataSetChanged, it didn’t work. So I googled a little bit and found that I need to override the getItemPosition method in PagerAdapter when removing item.

This is the doc from PagerAdapter

A data set change may involve pages being added, removed, or changing position. The ViewPager will keep the current page active provided the adapter implements the method getItemPosition(Object).

and description of getItemPosition

Called when the host view is attempting to determine if an item’s position has changed. Returns POSITION_UNCHANGED if the position of the given item has not changed or POSITION_NONE if the item is no longer present in the adapter.

Then I put it in my pager adapter, got a different behavior, but still not work correctly. Three thoughts came to me immediately:

  1. something wrong in my getItemPosition
  2. something wrong when I integrate with my other code.
  3. something wrong in FragmentStatePagerAdapter

For 1, it is easy to verify by printing logs and it looked good to me. 2, I don’t think so but still spend some time to check my code first. As expected, problem still existed. I always have a problem with Fragment lifecycle, everytime I thought I understand more, a new problem came out and breaks my understanding of fragment lifecycle. I tried hard to avoid go deep to the implementation of FragmentStatePagerAdapter, but this time I have no choice.

FragmentStatePagerAdapter

I am a little bit surprised by the short code length of it.

12345678910111213141516171819202122232425262728293031323334353637
//https://android.googlesource.com/platform/frameworks/support/+/refs/heads/master/v4/java/android/support/v4/app/FragmentStatePagerAdapter.javaprivate ArrayList<Fragment> mFragments = new ArrayList<Fragment>();@Overridepublic Object instantiateItem(ViewGroup container, int position) {    if (mFragments.size() > position) {        Fragment f = mFragments.get(position);        if (f != null) {            return f;        }    }    if (mCurTransaction == null) {        mCurTransaction = mFragmentManager.beginTransaction();    }    Fragment fragment = getItem(position);    ...    mFragments.set(position, fragment);    mCurTransaction.add(container.getId(), fragment);    return fragment;}@Overridepublic void destroyItem(ViewGroup container, int position, Object object) {    Fragment fragment = (Fragment)object;    if (mCurTransaction == null) {        mCurTransaction = mFragmentManager.beginTransaction();    }    ...    mSavedState.set(position, mFragmentManager.saveFragmentInstanceState(fragment));    mFragments.set(position, null);    mCurTransaction.remove(fragment);}

Here is what FragmentStatePagerAdapter does in instantiateItem and destroyItem, in short, it maintains an ArrayList mFragments which mFragments.get(i) returns the fragment that is in position i, null if fragment not in fragment manager. By default, ViewPager will keep one item before and after current item, so if you are at position 1 (0-based), mFragments will be like

01234F0F1F2nullnull

Remove item

What will happen if I remove F1 and call notifyDataSetChanged? the result of getItemPosition should be

123
F0: 0 (or POSITION_UNCHANGED?)F1: POSITION_NONEF2: 1

Right?

Then I expect mFragments become

01234F0F2nullnullnull

But from the source of FragmentStatePagerAdapter, I don’t think it has any handling of it. I copied the source and added some log to dump mFragments out, and find that it is like

StepWhat I see012341curr item F1F0F1F2nullnull2after remove F1, F2 becomes curr itemF0nullF2nullnull3swipe next, no item displayed but F4 instantiatednullnullF2F4null4swipe next, curr item F4nullnullF2F4F55swipe next, curr item F5nullnullnullF4F5

then the app crashed on step 5 when it tries to destroy F2.

Dafaq?

First of all, what happened in step 2? It just removed F1 and left F2 there, and ViewPager just display it?

Yes, something like that.

  1. When F1 is removed and called notifyDataSetChanged, ViewPager will call getItemPosition for each item, in the order of F0, F1, F2.
  2. When POSITION_NONE is returned for F1, adapter’s destroyItem is called and F1 is removed from FragmentManager and mFragments.
  3. Then for F2, it returned the new position 1, which match the current position, so ViewPager uses F2 directly, but leave mFragments in adapter not updated.
  4. After that, it should create F3 by calling instantiateItem on position 2, however, as mFragments still keeping F2 in position 2, it is directly returned and F3 is never created.
  5. In the first swipe after remove, ViewPager tries to display data in position 2 which the fragment F2 is already used in position 1. At the same time, F0 is removed from FragmentManager, F4 is created as item in position 3.
  6. Second swipe, position 3(F4) becomes current item, position 1(F2) removed from FragmentManager, F5 created for position 4.
  7. Third swipe, position 4(F5) becomes current item, ViewPager tried to destroy position 2, but position 2 is also F2, destroying that cause

    IllegalStateException: Fragment {} is not currently in the FragmentManager.

Workaround

When I tried to isolate the problem, I accidentally return POSITION_NONE for all item, and it actually gave the result I want without crash. It worked because all fragments are destroyed in notifyDataSetChanged and re-instantiated, it does the tricks but may cause other performance problem so I am looking for a better solution.

Possible fix

I have not start yet, but as mFragments in FragmentStatePagerAdapter is private, extending it and override some methods cannot change it at all. Good news is, you can copy the source and compile it yourself.

To fix this, you need to find a way to update mFragments after checking getItemPosition and destroyItem. My initial thoughts is to handle that in finishUpdate(). The new finishUpdate will become something like:

1234567891011121314151617181920
@Overridepublic void finishUpdate(ViewGroup container) {    if (mCurTransaction != null) {        mCurTransaction.commitAllowingStateLoss();        mCurTransaction = null;        mFragmentManager.executePendingTransactions();    }    ArrayList<Fragment> update = new ArrayList<Fragment>();    for (int i=0, n=mFragments.size(); i < n; i++) {        Fragment f = mFragments.get(i);        if (f == null) continue;        int pos = getItemPosition(f);        while (update.size() <= pos) {            update.add(null);        }        update.set(pos, f);    }    mFragments = update;}

One problem is that finishUpdate is also called in other places so need to make sure this change will not break other code and not causing performance issue.

About this post…

At first I just want to write a short notes on the problem I met during work, then I tried to isolate the problem and reproduce it, tried to find similar posts on stackoverflow, read the soure code to confirm the problem, and even tried to fix it… This is totally not my plan for this weekend but I actually quite enjoy reading the source code of support library.

Reference

  • Discussion on gplus thx Adam Powell for replying my post.
  • Source of ViewPager and FragmentStatePagerAdapter
  • https://code.google.com/p/android/issues/detail?id=37990
Mar 2nd, 2014
android, viewpager

0 0