美文网首页
Activity恢复时如何获取Fragment

Activity恢复时如何获取Fragment

作者: Rreply | 来源:发表于2018-10-10 12:08 被阅读69次

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

参考:你真的会用Fragment吗?Fragment复用的那些事儿

相关文章

网友评论

      本文标题:Activity恢复时如何获取Fragment

      本文链接:https://www.haomeiwen.com/subject/jluuaftx.html