你真的会用Fragment吗?Fragment复用的那些事儿
- 如未特殊说明,本文中的知识点适用于 Activity 重建的时候,即:
public void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState)// 略........if (savedInstanceState != null) {// 本文讨论的情况} else {// 非本文讨论的情况}// 略........
}
- 为减少不必要的代码,文章中的
fm
、FM
均指代FragmentManager
- 如果你已经能熟练的使用 findFragmentById、findFragmentByTag、putFragment、getFragment 的用法以及它们各自的使用场景那么本文可能并不适合你
概述
- 为什么要复用Fragment
- 为何避免使用 FM.getFragments
- FragmentManager.findFragmentById 的使用
- FragmentManager.findFragmentByTag 的使用
- ViewPager 复用之 FragmentManager.getFragment 的使用
一、 为什么要复用Fragment
根本原因只有一个:Activity 在重建的时候会恢复其包含的 FragmentManager ,FragmentManager 又会恢复其管理的 Fragment ,同理 Fragment 也会恢复其包含的 FragmentManager,层层递进,直到全部恢复
复用的好处:
- 避免显示错乱
- 避免重复添加
- 避免多余的内存占用
- 优化界面启动速度
- ........
所以复用还是相当有必要的,同时当我们知道了要复用的根本原因之后,如何复用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中返回的是 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();}
}
:
- 该方式比较适合 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;
@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
参数是我们在进行 add
或 replace
操作的时候指定的。
提示:
- tag 是可以重复的,因为该参数的之只是 Fragment 的一个成员变量,只是我们无法访问(访问权限 default)。
- 该方法总是返回 FragmentManager 中和该 tag 一致的最后一个 Fragment。也就是说如果有多个 Fragment 对象使用了同一个 tag 那么最后一个被添加的会被返回,所以不要为不同的 Fragment 对象指定相同的 tag。
- 不要为同一个 Fragment 实例对象指定在不同的操作中指定不同的 tag,不然会抛出异常,当然这种情况一般是发生在重复添加的情况下
六、与 ViewPager 配合时不要试图使用 FM.findFragmentByTag
上面的 findFragmentById
和 findFragmentByTag
在使用的时候其实都是有一些隐藏限制的:
- 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.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) { // 没有被添加到 FragmentManagerthrowException(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 为例,其他的同理
- 当存储状态的时候我们通过putFragment 记录下 FragmentA 的 mIndex, 使用的key 为字符串 "fragment:A"
- 当我们需要查找 A 的时候,先根据 字符串 "fragment:A"(putFragment时使用的值) 去 bundle 中查出我们在 fragmentManager 销毁前记录的 mIndex = 5
- 通过 mActivie 中得到 key = 5 的Fragment对象 即:Fragment A
- 由于 fragment.mIndex 和 FragmentManagerImpl.mActive 无法访问到所以才需要 getFragment 和 putFragment。
注意事项:
- getFragment 和 putFragment 必须成对使用。
- 在调用 putFragment 方法之前先保证该 fragment 是否已经添加到 FragmentManager 了(即fragment.mIndex >= 0),不然从源码可以得知会抛出异常。
七、总结
- 在写 Activity 和 Fragment 的代码时区分区分新建和恢复,在恢复的情况下先查找 Fragment,找不到再创建实例对象
- FM.getFragment 适合多个 Fragment 共用一个 ViewGroup 同时还无法为Fragment指定Tag的情况(如ViewPager)
- FM.findFragmentById 适合一个 ViewGroup 对应 一个 Fragment 的情况
- FM.findFragmentByTag 适合大多数情况,但需要在 add/replace 的时候为每个 Fragment 指定不同 tag
- 当有多个 Fragment 对象具有相同的 tag 时,通过 findFragmentByTag 得到的是最后被添加的 Fragment
- 当有多个 Fragment 对象共用同意个ViewGroup时,通过 findFragmentById 得到的是最后被添加的 Fragment
- putFragment 使用时先判断 Fragment 是否已经添加到 FragmentManager
最后附上一张图告诉你如何选择合适的方法来查找Fragment
查找Fragment方法选择.jpg
你真的会用Fragment吗?Fragment复用的那些事儿相关推荐
- ViewPager+Fragment+ViewPager+Fragment
最近一段时间,Android行业大不如从前轻松,企业要求越来越高了,就算入职了很多时候Android这块也不太受重视,现在Android开发者还能经常接到新需求就算是很幸运的事了,我最近的工作也是没什 ...
- android独特fragment,Android Fragment总结
Android Fragment小结 为何使用Fragment? 实现UI的灵活组建与拆分,与Activity配合可保持Activity的视图不变,转而操作Fragment,就像Activity的模块 ...
- android fragment 管理器,Android Fragment 與 Fragment管理器
Android Fragment 與 Fragment管理器 首先談談Fragement的需求 過去開發人員認為界面之間的跳轉只需要使用多個activity組成就行了: 例如下圖中,在Activity ...
- android 标题栏 fragment,切换Fragment 并更换标题栏
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 package com.kingberry.googlemaptracks.adapter; import java.util.ArrayList; im ...
- must implement OnFragmentInteractionListener/ Fragment与Activity,Fragment与Fragment之间的信息传递
出现这个问题的原因, 是Fragment关联的Activity没有实现OnFragmentInteractionListener接口. 那为什么要实现这个接口,以及怎样实现这个接口呢?让我们一步一步来 ...
- Fragment嵌套Fragment
问题1.fragment嵌套fragment不显示问题 通常时候,我们制作底部Tab切换,会用到fragment.即一个Activity下,使用4种fragment.这次遇到的问题是关于fragmen ...
- Activity与Fragment,以及Fragment与Fragment之间的数据通讯
Activity和Fragment无疑是Android开发中使用最多的组件,如果Activity使用了多个Fragment,需要在Activity与Fragment,以及Fragment与Fragme ...
- Fragment has not been attached yet Fragment 套 Fragment
Fragment has not been attached yet Fragment 套 Fragment 在商城项目中使用了 Fragment 套 Fragment的结构,大致框架如下图 Mall ...
- Android开发-Fragment嵌套Fragment
Android开发-Fragment嵌套Fragment 前言 使用依赖 远程仓库地址 布局实现 使用控件 xml代码 Java实现 效果图 项目地址 前言 在大多数公司中,他们会尽量少写Activi ...
最新文章
- SpringBoot启动标识修改
- java如何用c 的方法_JAVA如何调用C/C++方法
- 公众平台服务号、订阅号、企业号的相关说明
- HTML中a标签/超链接标签的下划线怎么去掉
- 【Java报错】记录一次 sun.misc.Unsafe.park(Native Method) Conflicting setter definitions for property 导致的内存泄露
- CF429E Points and Segments(欧拉回路)
- 持续集成持续部署持续交付_如何开始进行持续集成
- c#c#继承窗体_C#继承能力问题和解答 套装5
- 自动驾驶车辆转向控制(通过支持转角控制的EPS实现角速度控制)
- 在DialogFragment中显示大图片
- JavaWeb开发必会技巧1——导入jar包
- retrofit2 不创建对象直接返回字符串
- [转]SOAP 教程
- 全局变量反汇编与重定位
- Git 命令行(cygwin) + Git Extensions + Git Source Control Provider
- oracle 12c transaction guard,保障业务连续性的神器
- PS插件:灯光工厂安装教程
- 如何把psd格式转为html,将psd转化为HTML网页详情讲解
- Java数组的复制、扩容、删除
- AspenTech利用ExaGrid实现全球数据备份和恢复策略的现代化