Fragment修炼奥义之Fragment复用那些事

本文围绕Fragment复用展开,阐述复用原因是避免显示错乱、重复添加等。介绍获取已存在Fragment的方法,如不推荐的FragmentManager.getFragments(),分析其不可靠原因;还讲解了fm.findFragmentById()、fm.findFragmentByTag()的适用场景,以及与ViewPager配合时的处理方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述

作者:怪盗kidou
链接:https://www.jianshu.com/p/31f013df7580
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

  • 为什么要复用Fragment
  • 如何避免fm.getFragments
  • FragmentManager.findFragmentById的使用
  • FragmentManager.findFragmentByTag的使用
  • ViewPager复用之FragmentManager.getFragment的使用

一、为什么要复用Fragment

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

复用的好处:

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

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


##二、如何获取已经存在的Fragment ##
目前已经知道的方法如下:

  • 【不推荐】获取全部的已经添加到FragmentManager

    FragmentManager.getFragments()

  • 根据TAG查找Fragment

    FragmentManager.findFragmentByTag(String tag)

  • 根据ID查找Fragment

FragmentManager.findFragmentById(int id)
  • 【重点】根据key查找Fragment,这个适合与viewpager配合(划重点,要考)
    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中返回的是Cllections.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;
@Overridepublic 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();
}
}
  • 该方式比较适合 ViewGroup 和 Fragment 是一对一的情况下使用,当不满足该条件时可以使用后面介绍的 findFragmentByTag 方法。
  • 当 一个 ViewGroup 中 有多个 Fragment 时该方法会返回最后添加到该 ViewGroup 的 Fragment。

五、fm.findFragmentByTag()

当一个 ViewGroup 中有多个 Fragment 时 findFragmentById 可能就不是太好使了,这种情况下就需要我们使用 findFragmentByTag 了。
由于是通过 tag 查找已经添加到 FragmentManager 里的 Fragment 实例对象,所以和 containerViewId 也就没有关系了,当然了在我们添加 Fragment 的时候也要注意给 fragment 指定 tag。

方法签名:

public abstract Fragment findFragmentByTag(String tag);

用法示例:

private MainFragment mainFragment;
@Overridepublic 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 操作的时候指定的。

提示

  • tag 是可以重复的,因为该参数的之只是 Fragment 的一个成员变量,只是我们无法访问(访问权限 default)。
  • 该方法总是返回 FragmentManager 中和该 tag 一致的最后一个 Fragment。也就是说如果有多个 Fragment 对象使用了同一个 tag 那么最后一个被添加的会被返回,所以不要为不同的 Fragment 对象指定相同的 tag。
  • 不要为同一个 Fragment 实例对象指定在不同的操作中指定不同的 tag,不然会抛出异常,当然这种情况一般是发生在重复添加的情况下

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

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

  • findFragmentById 适用于一个萝卜一个坑的情
  • findFragmentByTag 使用于 可以指定为 Fragment 指定 tag 情况。

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

  • 由 ViewPager 所管理的 Fragment 使用的都是同一个 id ,即 ViewPager 的id。
  • 由于 ViewPager 来管理 Fragment 所以我们无法干预其添加移除的过程,所以没有办法为 fragment 指定 tag。
  • 这次针对ViewPager的这种情况我要介绍的方法是FragmentManager.getFragment()方法,与其配套使用的还有一个 FragmentManager.putFragment()方法。

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

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

正确的处理姿势示范:

private MainFragment mainFragment;
private SecondaryFragment secondaryFragment;


@Overridepublic 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 的相关操作
}


@Overrideprotected 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没有表示出来,但不代表不存在

以 图中 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。

注意事项:

  • getFragment 和 putFragment 必须成对使用。
  • 在调用 putFragment 方法之前先保证该 fragment 是否已经添加到 FragmentManager 了(即fragment.mIndex >= 0),不然从源码可以得知会抛出异常。

七、总结

  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
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值