Android知识rxjavaFragment

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

2018-05-24  本文已影响1128人  怪盗kidou

本文的主要目的介绍的是当使用ViewPager时如何查找Fragment的办法,同时介绍一下在使用Fragment时的一些注意事项,以及几种查找方法所适用的场景。

作者: @怪盗kidou
如需转载不得删除本文中的任何内容(含本段)
如果博客中有不恰当之处欢迎在原文中留言交流
https://www.jianshu.com/p/31f013df7580

大家好,好像距离上次发布博客好像又过去了大半年了(额,好像每次发博客都有这句话),不过还好我的博客从来不是以数量取胜。

我统计了一下:截止到2018年5月23号,只有11篇文章的博客访问量已经超过 63 万了!感谢大家的支持!

好的屁话不多说,继续看文章

约定

  1. 如未特殊说明,本文中的知识点适用于 Activity 重建的时候,即:
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState)
    // 略........
    if (savedInstanceState != null) {
        // 本文讨论的情况
    } else {
        // 非本文讨论的情况
    }
    // 略........
}
  1. 为减少不必要的代码,文章中的 fmFM 均指代 FragmentManager
  2. 如果你已经能熟练的使用 findFragmentById、findFragmentByTag、putFragment、getFragment 的用法以及它们各自的使用场景那么本文可能并不适合你

概述


一、 为什么要复用Fragment

根本原因只有一个:Activity 在重建的时候会恢复其包含的 FragmentManager ,FragmentManager 又会恢复其管理的 Fragment ,同理 Fragment 也会恢复其包含的 FragmentManager,层层递进,直到全部恢复

复用的好处:

  1. 避免显示错乱
  2. 避免重复添加
  3. 避免多余的内存占用
  4. 优化界面启动速度
  5. ........

所以复用还是相当有必要的,同时当我们知道了要复用的根本原因之后,如何复用Fragment也就变成 【如何查找已存在的Fragment】的问题了。


二、如何获取已经存在的Fragment

目前我知道的方法如下:

FragmentManager.getFragments() 
FragmentManager.findFragmentByTag(String tag)
FragmentManager.findFragmentById(int id) 
FragmentManager.getFragment(Bundle bundle,String key)
FragmentManager.putFragment(Bundle bundle, String key, Fragment fragment)

三、谨慎使用FragmentManager.getFragments() 方法

既然不推荐,那总是有原因的,在这个小节会花费比较大的篇幅,我会结合代码告诉你为什么不推荐。

理由一:内容不可控导致Crash

FragmentManager.getFragments() 会返回所有已经添加到 FragmentManager 中的 Fragment,这就可能导致这个列表中包含了非我们自己所定义的Fragment,你可能会有疑问界面上不就显示我自己定义的Fragment么?

首先我们应该清楚的认识到 Fragment 不单单是界面的载体,它也可以用来实现别的功能,比如 生命周期 的监听。比如图片加载库 Glide 以及 Android 最新的 Android 架构组件 中的 ViewModel 都采用了这种方式。

所以如果我们的 Fragment 是和 ViewPager组合使用并且直接将包含这些实例对象(比如 ViewModel 用到 HolderFragment) FragmentManager.getFragments() 的结果丢给 FragmentPagerAdapter 的话那么就会达成本博客的第一项成就:Fragment重复添加

throw new IllegalStateException("Fragment already added: " + fragment)

理由二:顺序不可控

下面的这段代码我相信大家都很熟悉,就算自己没有写过也看别人写过

MainFragment mainFragment = (MainFragment) fm.getFragments().get(0)
// 略.......
SecondaryFragment secondaryFragment = (SecondaryFragment) fm.getFragments().get(1)
// 略.......

这样的写法就会帮助你达成第二项成就:类型转换异常

throw new ClassCastException("Cannot cast android.arch.lifecycle.HolderFragment to MainFragment")

ViewModel相关源码那里可以知道FragmentManager.getFragments() 中包含了其他的Fragment,而这些Fragment的位置往往是不固定,以ViewModel为例,HolderFragment的位置是由初始化的时机决定的。

也就是说你调整了一下 ViewModel 初始化的调用顺序或者在Kotlin项目中将 lateinit 改成了 by lazy 都可能会发生这样的Crash!就 lateinit 改成 by lazy 这条就是我前不久在做项目时真实遇到的。

理由三:26.x.y 版本中行为发生变更

在 版本25 中 Activity 是新建的情况下 返回的是 null ,在版本26中返回的是 Collections.EmptyList() ,前面我在维护公司项目时引入了 ROOM 然后有几个界面崩溃了!

此刻我的心情

经过排除发现而问题就出在下面的这段代码中。

mFragments = new ArrayList<>();
if(fm.getFragments() == null){
    mFragments.add(new MainFragment())
    mFragments.add(new SecondaryFragment())
}else{
    mFragments.addAll(fm.getFragments())
}
mViewPager.setAdapter(new MyViewPagerAdapter(fm, mFragments))
mTabLayout.setupWithViewPager(mViewPager)
// .....
mTabLayout.getTabAt(0).setText("MainFragment")
// .....

原因就是版本26下,返回的不是 null 导致 mFragments 是空的,自然mTabLayout里面是没有Tab的,所以导致了 空针异常,如果这段代码不依赖 getFragments 方法的话其实是没有问题的。

不知道大家有没有注意,如果这个Activity也使用ViewModel,那么还可能会顺带达成上面的 成就一和成就二

扎心了老铁

通过上面的一些例子我们知道了既然直接通过 FM.getFragments() 不可靠,那么通过其他几种方式来获取我们想要找的 Fragment 实例结果如何呢,接着往下看。


四、FM.findFragmentById()

该方法是用过 Fragment 所在的 ViewGroup 的 id(containerViewId) 来查找 Fragment,适合一个 ViewGroup 中只有一个 Fragment 的情况。

方法签名:

public abstract Fragment findFragmentById(@IdRes int id);

用法示例:

private MainFragment mainFragment;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (savedInstanceState != null) {
        mainFragment = (MainFragment) getSupportFragmentManager()
                // 这个ID和下面添加 fragment 时指定的 id 要一致
                .findFragmentById(android.R.id.content);
    } else {
        mainFragment = new MainFragment();
        getSupportFragmentManager().beginTransaction()
                .add(android.R.id.content, mainFragment)
                .commit();
    }
}

:


五、FM.findFragmentByTag()

当一个 ViewGroup 中有多个 Fragment 时 findFragmentById 可能就不是太好使了,这种情况下就需要我们使用 findFragmentByTag 了。

由于是通过 tag 查找已经添加到 FragmentManager 里的 Fragment 实例对象,所以和 containerViewId 也就没有关系了,当然了在我们添加 Fragment 的时候也要注意给 fragment 指定 tag。

方法签名:

public abstract Fragment findFragmentByTag(String tag);

用法示例:

private MainFragment mainFragment;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (savedInstanceState != null) {
        mainFragment = (MainFragment) fm.findFragmentByTag(MainFragment.TAG);
    } else {
        mainFragment = new MainFragment();
        fm.beginTransaction()
                // 在添加的时候给其制定 tag,不然到时候上面的语句就没用了
                .add(android.R.id.content, mainFragment, MainFragment.TAG)
                .commit();
    }
}

上面就是一个很简单的用 TAG 来获取Fragment 的例子,这里需要注意的就是 tag 参数是我们在进行 addreplace 操作的时候指定的。

提示:


六、与 ViewPager 配合时不要试图使用 FM.findFragmentByTag

上面的 findFragmentByIdfindFragmentByTag 在使用的时候其实都是有一些隐藏限制的:

但是很不巧 ViewPager 与这两个情况都匹配不上,原因:

这次针对 ViewPager 的这种情况我要介绍的方法是 FragmentManager.getFragment()方法,与其配套使用的还有一个 FragmentManager.putFragment()方法。

你去搜 【ViewPager find fragment】 可能别人告诉你的 调用 makeFragmentName 生成 tag 或者用 findFragmentByTag("android:switcher:" + viewPager.getId() + ":" + viewPager.getCurrentItem()) 的那些做法就不要再用了!

// FragmentPagerAdapter.java
private static String makeFragmentName(int viewId, long id) {
    return "android:switcher:" + viewId + ":" + id;
}

正确的处理姿势示范:

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);
    }
}

两个方法的源码如下:

// FragmentManager.java,摘自版本 27.1.1
@Override
public void putFragment(Bundle bundle, String key, Fragment fragment) {
    if (fragment.mIndex < 0) { // 没有被添加到 FragmentManager
        throwException(new IllegalStateException("Fragment " + fragment
                + " is not currently in the FragmentManager"));
    }
    bundle.putInt(key, fragment.mIndex);
}

@Override
public Fragment getFragment(Bundle bundle, String key) {
    int index = bundle.getInt(key, -1);
    if (index == -1) {
        return null;
    }
    Fragment f = mActive.get(index);
    if (f == null) {
        throwException(new IllegalStateException("Fragment no longer exists for key "
                + key + ": index " + index));
    }
    return f;
}

原理解析:

先放两张图,然后结合图片解析

Fragment 在 FragmentManager 中的存储形式

上图只是给出了我们已经知道的,未知的 Fragment 没有表示出来,但不代表不存在

getFragment、putFragment.jpg

以 图中 Fragment A 为例,其他的同理

  1. 当存储状态的时候我们通过putFragment 记录下 FragmentA 的 mIndex, 使用的key 为字符串 "fragment:A"
  2. 当我们需要查找 A 的时候,先根据 字符串 "fragment:A"(putFragment时使用的值) 去 bundle 中查出我们在 fragmentManager 销毁前记录的 mIndex = 5
  3. 通过 mActivie 中得到 key = 5 的Fragment对象 即:Fragment A
  4. 由于 fragment.mIndex 和 FragmentManagerImpl.mActive 无法访问到所以才需要 getFragment 和 putFragment。

注意事项:


七、总结

  1. 在写 Activity 和 Fragment 的代码时区分区分新建和恢复,在恢复的情况下先查找 Fragment,找不到再创建实例对象
  2. FM.getFragment 适合多个 Fragment 共用一个 ViewGroup 同时还无法为Fragment指定Tag的情况(如ViewPager)
  3. FM.findFragmentById 适合一个 ViewGroup 对应 一个 Fragment 的情况
  4. FM.findFragmentByTag 适合大多数情况,但需要在 add/replace 的时候为每个 Fragment 指定不同 tag
  5. 当有多个 Fragment 对象具有相同的 tag 时,通过 findFragmentByTag 得到的是最后被添加的 Fragment
  6. 当有多个 Fragment 对象共用同意个ViewGroup时,通过 findFragmentById 得到的是最后被添加的 Fragment
  7. putFragment 使用时先判断 Fragment 是否已经添加到 FragmentManager

最后附上一张图告诉你如何选择合适的方法来查找Fragment


查找Fragment方法选择.jpg
上一篇下一篇

猜你喜欢

热点阅读