使用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:
- something wrong in my getItemPosition
- something wrong when I integrate with my other code.
- 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
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
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
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.
- When F1 is removed and called notifyDataSetChanged, ViewPager will call getItemPosition for each item, in the order of F0, F1, F2.
- When POSITION_NONE is returned for F1, adapter’s destroyItem is called and F1 is removed from FragmentManager and mFragments.
- 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.
- 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.
- 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.
- Second swipe, position 3(F4) becomes current item, position 1(F2) removed from FragmentManager, F5 created for position 4.
- 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
- 使用ViewPager+FragmentAdapter 增删Fragment 异常及bug
- ViewPager+FragmentAdapter+Fragment
- Viewpager使用FragmentAdapter
- Fragment viewPager 自定义 异常捕获
- fragment嵌套viewpager viewpager嵌套fragment 的bug
- viewpager+FragmentAdapter实现App主界面Tab
- 20 ViewPager demo5,6:FragmentAdapter 导航数据
- viewpager+fragmentadapter实现微信界面
- 使用ViewPager+Fragment+Indicator
- ViewPager + Fragment使用
- ViewPager+Fragment的使用
- 使用ViewPager加载Fragment
- ViewPager + Fragment 使用
- 使用Fragment填充ViewPager
- ViewPager+Fragment的使用
- Fragment+ViewPager使用示例
- ViewPager+Fragment 使用问题
- ViewPager+Fragment 使用问题
- Java中重载与重写的区别
- Java连接数据库第一版
- python-xgboost调参经验
- 旋转数组
- Seccon CTF 2016 部分Writeup.md
- 使用ViewPager+FragmentAdapter 增删Fragment 异常及bug
- ViewPager+GridView实现商品分类
- Android 自定义view探索
- 同时安装office2016与visio2016
- 第十六周2
- 股票入门基础知识1:什么是股票和股份?
- 编译原理之小记录
- 球盒问题之三:n分解成m个正数和所有组合
- [BZOJ1385][Baltic2000]Division expression