Activity恢复时如何获取Fragment
Activity 在重建的时候会恢复其包含的 FragmentManager ,FragmentManager 又会恢复其管理的 Fragment ,同理 Fragment 也会恢复其包含的 FragmentManager,层层递进,直到全部恢复。
本文主要讨论Activity重建的时候如何获取Fragment的。即如下情况:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState)
FragmentManager fm = getSupportFragmentManager();
if (savedInstanceState != null) { // 本文讨论的情况
} else {
// 非本文讨论的情况
}
}
具体通过四种方法来获取复用Fragment。
- fm.getFragments
- fm.findFragmentById
- fm.findFragmentByTag
- fm.putFragment与getFragment
下面将对这四种情况分别加以分析说明。
fm.getFragments
首先来一个总结,不建议使用该方法获取Fragment。理由有如下三点:
1. 得到的Fragment可能包含你不需要的Fragment
getFragments方法获取的是所有已经添加到FragmentManager中的Fragment。但是这个FragmentManager中保存的不只是我们定义的Fragment,还有可能会包含其他用途的Fragment。
Fragment不仅仅是界面的载体,同时它可以用来实现生命周期的监听,因为它的生命周期和Activity是一致的,当我们不好监听Activity的生命周期的时候,就可以使用Fragment来辅助监听。图片加载库Glide和Android Jet Pack中的ViewModel都使用了这种模式。
2. 得到的Fragment列表中Fragment顺序是不可控的
前段时间就遇到过这样的bug
FragmentA fragmentA = (FragmentA) fm.getFragments().get(0)
FragmentB FragmentB = (FragmentB) fm.getFragments().get(1)
然后就出现了类型转换异常,FragmentA不能被强制转换成FragmentB。
当时我百思不得其解,为什么会出现这个问题啊,我明明是按照Fragment添加到FragmentManager的顺序去获取Fragment的啊。
直到我将getFragments获取到的Fragment列表打印出来才发现,其中的Fragment顺序和我添加到FragmentManager中的顺序是不一致的。
也就是说因为getFragments中获取到的Fragment包含了你不想要的Fragment,而这些Fragment的初始化时机又是不可预料的,所以就不能通过Fragment列表准确定位你需要的Fragment。
3. 26.x.y版本中返回值发生变更
这个情况我没有遇到过,参考文章的作者怪盗kidou遇到过,就顺便写上了。
在版本25中Activity是新建的请款下,getFragments返回的是null,然后到了26版本,getFragments返回的就是Collectiions.EmpytList()。。。
这就导致原来基于null判断的程序出现bug。
fm.findFragmentById()
这个方法是通过Fragment中所在的ViewGroup的Android:id定义的id来查找,适合一个ViewGroup中只存在一个Fragment的情况。
<FrameLayout
android:id="@+id/fragment_container"
android:layout_height="match_parent"
android:layout_width="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android" />
FragmentManager fm = getSupportFragmentManager();
Fragment fragment = fm.findFragmentById(R.id.fragment_container);
//将fragment放入activity中
if (fragment == null) {
fragment = ArticleDetailFragment.newInstance(detailData);
fm.beginTransaction()
.add(R.id.fragment_container, fragment)
.commit();
}
当然,如果一个ViewGroup中有多个Fragment的情况下也是可以使用的,不过这个时候获取的就是最后添加到ViewGroup中的Fragment了。
fm.findFragmentByTag()
该方法就是用来处理findFragmentById不适用的情况的。因为是通过Tag来查找Fragment,所以ViewGroup的id也就没用了。
if (savedInstanceState != null) {
mainFragment = (MainFragment) fm.findFragmentByTag(MainFragment.TAG);
} else {
mainFragment = new MainFragment();
fm.beginTransaction()
.add(android.R.id.content, mainFragment, MainFragment.TAG)
.commit();
}
需要注意的地方:
- tag是可以重复的,因为这个参数只是Fragment的一个成员变量,只是我们无法访问(访问权限default)。
- 如果有多个Fragment的tag是一样的,那么返回的则是最后一个添加的tag相同的fragment。
- 不要为同一个Fragment实例对象在不同的操作中指定不同的tag,否则会出现异常。当然这种情况一般是出现在重复添加的情况下。
fm.getFragment与fm.putFragment
有一种情况下是不能够使用getFragmentById和getFragmentByTag,那就是使用ViewPager管理Fragment的情况。
- 因为ViewPager在布局文件中使用的Android:id是不变的,但是其容纳的Fragment是可以不断变化的,这种情况下就不能通过id来查找Fragment。
- 使用ViewPager来添加移除Fragment的时候,我们是不能够在其中添加Tag的,所以getFragmentByTag也是不能使用的。
以前查找ViewPager中查找Fragment的时候的确发现了如下通过在FragmentPagerAdapter中强行设置tag来查找Fragment的情况。
// FragmentPagerAdapter.java
private static String makeFragmentName(int viewId, long id) {
return "android:switcher:" + viewId + ":" + id;
}
这种方法是可用的,但是太过麻烦,可以使用fm.putFragment和fm.getFragment来处理这种情况。
这两个方法的使用如下:
private MainFragment mainFragment;
private SecondaryFragment secondaryFragment;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
mainFragment = (MainFragment) fm.getFragment(savedInstanceState, MainFragment.TAG);
secondaryFragment = (SecondaryFragment) fm.getFragment(savedInstanceState, SecondaryFragment.TAG);
}
if (mainFragment == null) {
mainFragment = new MainFragment();
}
if(secondaryFragment == null){
secondaryFragment = new SecondaryFragment()
}
// ViewPager 的相关操作
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (mainFragment.isAdded()) {
fm.putFragment(outState, MainFragment.TAG, mainFragment);
}
if (secondaryFragment.isAdded()) {
fm.putFragment(outState, SecondaryFragment.TAG, secondaryFragment);
}
}
可能有朋友会感觉这个和getFragmentByTag那么像呢,好像没什么区别。
重点来了,因为在ViewPager中添加和移除Fragment是由ViewPager控制的,所以像是
fm.beginTransaction()
.add(android.R.id.content, mainFragment, MainFragment.TAG)
.commit();
这种方法就不能被使用了,既然无法在添加fragment的时候设置tag,那我们就不能够通过tag直接从FragmentManager中的Fragment列表中获取了。
那putFragment和getFragment方法是怎么做到这一点的呢?
我们查看下这两个方法的源码:
// FragmentManager.java,摘自版本 27.1.1
@Override
public void putFragment(Bundle bundle, String key, Fragment fragment) {
// 没有被添加到 FragmentManager
if (fragment.mIndex < 0) {
throwException(new IllegalStateException("Fragment " + fragment
+ " is not currently in the FragmentManager"));
}
//注意!!!此处是把Tag和Fragment的mIndex作为一组数据存储
bundle.putInt(key, fragment.mIndex);
}
@Override
public Fragment getFragment(Bundle bundle, String key) {
int index = bundle.getInt(key, -1);
//当bundle中不存在该Tag时
if (index == -1) {
return null;
}
//从mActive中获取Fragment
Fragment f = mActive.get(index);
if (f == null) {
throwException(new IllegalStateException("Fragment no longer exists for key "
+ key + ": index " + index));
}
return f;
}
通过上述源码,我们可以看出,putFragment将待存储的Fragment的Tag和mIndex作为一组数据存储在bundle中,然后在getFragment方法内先从bundle中取出对应Tag的mIndex,最后根据这个mIndex从mActive中取出对应的Fragment。
mActive是真正存储Fragment的对象,但是我们不能够直接使用Tag从中取出,因为ViewPager是使用mIndex来作为key值存储Fragment的。所以我们只能够退而求其次,将Tag和mIndex联系起来,达到间接使用Tag取出Fragment的效果。
下面是对上述源码及步骤的图形化表示:
Fragment存储形式.png Fragment获取步骤.jpg
注意事项:
- getFragment和putFragment方法必须成对使用。
- 在调用putFragment之前,必须保证该Fragment已经被添加到了FragmentManager中,即其mIndex >= 0,否则会抛出异常。
if (fragment.mIndex < 0) {
throwException(new IllegalStateException("Fragment " + fragment
+ " is not currently in the FragmentManager"));
}
总结
- 在使用Activity和Fragment的时候,应该分清楚恢复和新建的作用,如果能够从FragmentManager中恢复,就优先使用恢复的Fragment,不能够恢复再新建。
- 不要使用getFragments方法来获取Fragment,可能会导致各种意想不到的错误。
- 在ViewGroup中只有一个Fragment的时候,优先使用getFragmentById方法。如果该ViewGroup中有多个Fragment,则返回最后一个添加到FragmentManager的Fragment。
- 在ViewGroup中有多个Fragment的时候,应该考虑使用getFragmentByTag方法,当有多个Fragment的Tag相同的时候,则返回最后一个添加到FragmentManager中的Fragment。
-
在使用ViewPager的时候,应该使用putFragment和getFragment这一对方法。
最后上一张比较形象的查找步骤图。
查找步骤图.jpg