简介:一套以Fragment嵌套实现多标签页面的完整Android工程资源,面向初、中级开发者,重点解决标签栏与翻页组件联动、子片段生命周期管理、事件传递与状态恢复等开发难点。压缩包共330个文件、约10.42MB,其中25份Java源码与40份界面布局配置是主要学习对象,175张图片覆盖界面切图与运行截图,另有编译生成的class文件、依赖jar包、so库以及可安装的apk,便于对照源码检查构建结果。工程涵盖登录注册、首页片段、分类标签条、图文绘制等多个模块,演示了用子片段管理器执行片段增删替换、以状态保存方式优化翻页切换,以及兼容性适配等做法。项目还保留调试信息,并配有按源码、资源与构建输出划分的目录结构。已有1203人学习下载,适合作为快速搭建多标签框架、深入理解片段嵌套机制的参考。
1. Fragment嵌套Fragment实现多tab页面:一个过渡动画暴露出的生命周期分水岭
Fragment嵌套Fragment实现多tab页面,这几个字背后藏着一道很多开发都会答错的选择题。一个常见场景是:底部导航进入二级页面后,页面内再挂三个横向tab,每个tab自带列表、筛选条件,甚至需要独立的返回栈。图省事把这三个tab全交给Activity的FragmentManager,看起来能跑,但外层Fragment一旦因内存回收或配置变更重建,内部的tab页面状态就会一排排地丢。更隐蔽的是,从二级页面返回上一级时,tab页的滚动位置、输入框内容会悄悄复原到初始状态。问题不在Fragment本身,而在嵌套层级里的FragmentManager用错了对象。这套方案的落地要点就一句话:让外层Fragment用childFragmentManager管理子Fragment,让子Fragment的生命周期归属父级而不是Activity。下面从原理到踩坑逐个说清,照着复现就能用。
2. 为什么必须嵌套而不是全挂到Activity:getChildFragmentManager()的取舍
2.1 “单Fragment+多View”方案为什么不适合复杂多tab
多tab页面最简单的实现是单个Fragment里放一个ViewPager2或一组LinearLayout,tab切换时用setVisibility控制View显隐。这种方案在tab数量少、每个tab页只是静态内容时完全够用,代码量也小。但一旦单个tab内部出现独立的列表加载、下拉刷新、网络请求回调,甚至自身嵌套子Fragment时,单View方案的缺点就暴露了:所有tab页的代码堆在一个Fragment里,onCreateView和onViewCreated的体积快速膨胀,逻辑耦合在所难免。
更重要的是生命周期隔离。一个Fragment对应一套生命周期方法,多个tab页面如果用多个View去承载,它们的onStart、onStop永远跟随宿主Fragment,无法独立管理。比如tab A在后台还在跑轮询,tab B已经停止交互,你却没法单独停掉A的定时器。用Fragment嵌套Fragment的方案,每个tab都是一个独立的Fragment,拥有自己的生命周期回调和方法入口,这也是这个标题所指向的架构核心。
2.2 getChildFragmentManager()与getFragmentManager()的本质区别
嵌套Fragment的落地关键在于:在哪个Fragment内部add子Fragment,就用哪个Fragment的getChildFragmentManager()。外层Fragment要添加内层tab Fragment时,必须使用外层Fragment实例上的getChildFragmentManager()方法。如果错用getFragmentManager()或getSupportFragmentManager(),子Fragment会被挂到Activity的FragmentManager上,生命周期直接与Activity绑定。
用一张表说明区别:
| 获取方式 | 所属管理器 | 生命周期跟随对象 | 适用场景 |
|---|---|---|---|
| getSupportFragmentManager() | Activity级管理器 | Activity | 在Activity中管理页面级Fragment |
| getChildFragmentManager() | Fragment内部管理器 | 宿主Fragment | 在Fragment内部管理其子Fragment |
| getParentFragment() | 返回父Fragment实例 | 无 | 子Fragment中获取宿主Fragment |
实际编码中常见的问题是子Fragment内部需要操作父Fragment时,用getParentFragment()获取父级实例;需要再往下一层嵌套时,就继续在子Fragment上调用getChildFragmentManager()。这一层一层的归属关系就是多层tab页面状态稳定的基石。
这里有个直观的验证方法:在父Fragment的onStart里打日志,再观察子Fragment的onStart触发时机。用Activity级manager添加的子Fragment会与Activity同周期,onStart会先于父Fragment触发;用childFragmentManager添加的子Fragment会跟随父Fragment,父onStart之后再触发。这个顺序差在页面过渡动画里体现得最明显——子Fragment如果先启动动画,而父Fragment还没就绪,就会出现白屏闪烁。这也是我日常排查嵌套Fragment问题时最先看的日志顺序。
3. 搭建嵌套多tab的最小可运行Demo:外层Fragment加三个子Fragment
3.1 外层Fragment的布局与基本结构
先从布局开始。外层Fragment的布局包含一个tab栏和一个内容容器,tab栏可以是LinearLayout加三个TextView,也可以是RecyclerView,这里用最直白的LinearLayout便于理解:
<!-- fragment_outer_tab.xml --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <LinearLayout android:id="@+id/tab_bar" android:layout_width="match_parent" android:layout_height="48dp" android:orientation="horizontal" android:gravity="center_vertical"> <TextView android:id="@+id/tab_one" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:gravity="center" android:text="页面一" /> <TextView android:id="@+id/tab_two" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:gravity="center" android:text="页面二" /> <TextView android:id="@+id/tab_three" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:gravity="center" android:text="页面三" /> </LinearLayout> <FrameLayout android:id="@+id/content_container" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> </LinearLayout>容器选择FrameLayout即可,子Fragment切换时通过show/hide控制显隐,不会影响容器自身的测量和布局逻辑。至于为什么不推荐在tab栏上叠加ViewPager2,原因放到最后一章单独说明,先把手写的hide/show方案跑通。
3.2 用childFragmentManager添加子Fragment的完整代码
外层Fragment的Java代码是核心。先在onCreateView里inflate布局,然后在Activity的config change发生时通过savedInstanceState判断是否已经添加过子Fragment,避免重复添加。子Fragment使用静态工厂方法创建,参数通过Bundle传递,这是官方推荐的方式,能避免无参构造带来的状态恢复异常:
public class OuterTabFragment extends Fragment { private static final String TAG_ONE = "TAG_ONE"; private static final String TAG_TWO = "TAG_TWO"; private static final String TAG_THREE = "TAG_THREE"; private Fragment[] tabFragments = new Fragment[3]; private int currentTab = 0; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_outer_tab, container, false); setupTabBar(root); switchTab(0); return root; } private void setupTabBar(View root) { root.findViewById(R.id.tab_one).setOnClickListener(v -> switchTab(0)); root.findViewById(R.id.tab_two).setOnClickListener(v -> switchTab(1)); root.findViewById(R.id.tab_three).setOnClickListener(v -> switchTab(2)); } private void switchTab(int position) { if (position == currentTab) { return; } FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); if (tabFragments[position] == null) { tabFragments[position] = createTabFragment(position); transaction.add(R.id.content_container, tabFragments[position], "TAB_" + position); } else { transaction.show(tabFragments[position]); } if (tabFragments[currentTab] != null) { transaction.hide(tabFragments[currentTab]); } transaction.commitAllowingStateLoss(); currentTab = position; } private Fragment createTabFragment(int position) { switch (position) { case 0: return ChildTabFragmentOne.newInstance("参数一"); case 1: return ChildTabFragmentTwo.newInstance("参数二"); case 2: return ChildTabFragmentThree.newInstance("参数三"); default: throw new IllegalArgumentException("unknown tab position: " + position); } } }这段代码里,tabFragments数组保存了三个子Fragment的实例引用。第一次切换到某个tab时,实例为空,创建后调用add添加到容器;后续再切换时,直接show/hide,不重新创建。FragmentTransaction的commitAllowingStateLoss()在这里用是安全的,因为用户点击tab发生在页面可见状态,但后续状态恢复时如果先触发onSaveInstanceState再做commit,用commit()就可能抛异常,两者的区别在第四章展开。
子Fragment的写法没有特殊要求,继承Fragment,正常实现onCreateView。需要注意的一个细节是:子Fragment内部如果需要获取宿主Fragment的信息,用getParentFragment()强转成对应类型,不要用getActivity()去外面找。
3.3 切换tab时的显示隐藏与状态保留策略
show/hide方案的核心好处是状态保留。子Fragment一旦添加过一次,它的View就留在容器里,onSaveInstanceState也不受影响,滚动位置、输入框内容、网络列表数据都还在。缺点是内存占用,三个tab页面每个都持有自己的View层级,如果每个tab内部还有大量图片或列表,整体占用的内存就会叠加。
实际项目中如果tab数量超过4个、每个tab又是复杂页面,我一般会把hide/show方案换成ViewPager2加setOffscreenPageLimit控制预加载数量,这个方案在第六章给出具体代码和参数设置。如果tab数量稳定在3个左右,hide/show方案更简单、没有ViewPager的滑动误触问题,也更好控制切换时机。
懒加载的逻辑在这种方案里也要重新考虑:子Fragment的onResume不代表用户可见,因为被hide的Fragment不会触发onResume,但也不会触发onHiddenChanged之外的回调。可见性判断要用onHiddenChanged配合isVisible组合实现,这段逻辑放在第五章说。
4. 生命周期与状态恢复:嵌套层级里的回调顺序和保存机制
4.1 父子Fragment生命周期的串联规律
嵌套Fragment的生命周期遵循一个规则:父Fragment的状态变化会带动子Fragment按相同顺序变化。父onStart时,子Fragment依次onStart;父onStop时,子Fragment依次onStop。父Fragment被detach或remove时,子Fragment也会被销毁。这个规律保证了子页面的运行状态与宿主页面完全同步。
实际操作中,有一个地方最容易踩坑:在父Fragment的onCreateView里直接new子Fragment并add,而不做savedInstanceState判断。当Activity因为配置变更(如旋转屏幕)重建时,父Fragment会被重新创建,onCreateView再次执行,于是子Fragment被重复添加。表现就是页面出现重叠,多个一样的tab堆在一起。原因就在这里:Activity重建后,FragmentManager会尝试恢复之前保存的Fragment实例,但你又在onCreateView里新add了一份,新旧两份共存。解决方式在3.2的代码里已经体现:不依赖savedInstanceState做判断,而是依赖childFragmentManager里的现有实例。前面的switchTab(0)里,tabFragments[0]如果为null才创建,但Activity重建后tabFragments数组会被置空,子Fragment实际上已经被FragmentManager恢复,此时需要从FragmentManager里查已有的实例。
改造方法如下:
@Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); FragmentManager childManager = getChildFragmentManager(); if (savedInstanceState != null) { Fragment f0 = childManager.findFragmentByTag("TAB_0"); Fragment f1 = childManager.findFragmentByTag("TAB_1"); Fragment f2 = childManager.findFragmentByTag("TAB_2"); tabFragments[0] = f0; tabFragments[1] = f1; tabFragments[2] = f2; } }这段逻辑要在switchTab之前执行,确保onCreateView里第一次调用switchTab时,tabFragments数组里已经有恢复出来的实例。配合3.2里switchTab中“实例为null才add”的判断,能完全避免重复添加。
4.2 onSaveInstanceState与状态恢复的边界
嵌套Fragment的状态恢复是由FragmentManager自动处理的,但有两个边界条件要注意。第一,FragmentManager的自动恢复只对Transaction中已提交且携带tag的Fragment生效,如果add时没有设置tag,恢复时findFragmentByTag会返回null,此时tabFragments数组从FragmentManager里找不到旧实例,只能新建一个,状态就丢了。所以add时第三个参数“TAB_" + position这个tag必须写。
第二,commitAllowingStateLoss和commit的时机。如果Activity已经执行了onSaveInstanceState,后续再调用commit(),系统会抛IllegalStateException,因为无法保证状态能完整恢复。commitAllowingStateLoss不会抛异常,但代价是极端情况下(进程被系统杀死后恢复),本次事务的Fragment可能不会被恢复。日常tab切换用commitAllowingStateLoss是稳妥的;涉及权限申请结果、异步回调返回后再做Fragment操作时,要在onResumeFragments里执行,或者用commitAllowingStateLoss保证不崩溃,具体取舍要看业务容忍度。
还有一个细节经常被忽略:子Fragment自己的onSaveInstanceState只在子Fragment被重新创建前保存,如果父Fragment一直不销毁,子Fragment的状态一直留在内存里;但如果系统在后台把进程杀了,所有未保存的Fragment状态都会丢失。这种极端情况下的恢复逻辑,依赖的是Activity的savedInstanceState传递下来的完整FragmentManager状态,无法在代码层面完全保障,只能尽可能在onSaveInstanceState里保存关键业务数据(比如选中的tab位置、列表首屏数据)。
5. 嵌套Fragment常见问题排查:重叠、懒加载失效与空指针都在这里
5.1 页面重叠:多个同名子Fragment堆叠在同一个容器里
现象:从二级页面返回后,tab容器里出现了两个甚至三个相同的页面叠在一起,点tab切换时界面错乱。
原因:最常见的是父Fragment重建时,没有先检查childFragmentManager里是否已经有同tag的子Fragment,直接在onCreateView里无脑add了一份新的。Activity旋转、内存回收后恢复、Fragment回退栈操作,都会触发父Fragment的onCreateView重新执行。
解决:标准做法是本章4.1里的逻辑。在onCreate里先用findFragmentByTag查找,找到就直接复用,找不到再创建。同时,add子Fragment的代码要放在条件判断里面,不能无条件执行。代码结构上,建议把switchTab逻辑抽成独立方法,在onCreateView里只调用一次,保证第一次切换tab时不会重复add。
5.2 懒加载失效:onResume已经触发了,页面还是空白
现象:设置了懒加载,但切换到隐藏tab时,网络请求已经发出去,甚至页面已经渲染了;或者反过来,切到可见tab时onResume没触发,数据一直没加载。
原因:很多老教程用setUserVisibleHint来实现懒加载,这个API在AndroidX的Fragment中已经失效,尤其和ViewPager2配合时,setUserVisibleHint的调用时机和实际可见性完全不同步。加上Fragment嵌套Fragment后,子Fragment的可见性不再只由ViewPager决定,还受父Fragment的show/hide影响,单纯依赖onResume或setUserVisibleHint都会出错。
解决:在子Fragment里重写onHiddenChanged,再配合isVisible判断。onHiddenChanged在hide/show切换时会触发,true表示从隐藏变为可见。再结合onResume处理首次创建时的可见逻辑:
@Override public void onResume() { super.onResume(); if (isVisible() && !isHidden()) { loadData(); } } @Override public void onHiddenChanged(boolean hidden) { super.onHiddenChanged(hidden); if (!hidden && isVisible()) { loadData(); // 这里只加载一次,首次加载由onResume负责 } }注意loadData里要自己做防重复判断,用一个布尔字段标记首次加载是否完成。纯粹用onHiddenChanged有个坑:子Fragment第一次被add时,onHiddenChanged不会触发,所以首次加载还依赖onResume里的逻辑。两个回调交叉配合才能覆盖全部可见场景。
5.3 getActivity()返回null,子Fragment里直接崩溃
现象:子Fragment点击按钮后,执行getActivity().findViewById()或getActivity().getSupportFragmentManager(),报空指针。
原因:异步回调里持有Fragment的引用,当Activity已经销毁但Fragment还存活时,getActivity()返回null。嵌套Fragment场景更明显:父Fragment在回退栈里被detach,子Fragment跟着离开,但异步回调(网络、延迟任务)还没回来,此时Fragment实例还在,Activity已经不在前台了。
解决:在异步回调里对getActivity()做null判断,用getContext()替代getActivity()做UI操作。Fragment在API 26之后有requireActivity(),它会抛异常而不是返回null,适合在确定Activity存活时使用。通用写法是:
Activity activity = getActivity(); if (activity == null || activity.isFinishing()) { return; // 直接丢弃回调结果 }5.4 状态恢复后滚动位置丢失:列表回到顶部
现象:从三级页面返回二级的多tab页面时,tab页内的RecyclerView滚动位置回到了顶部,之前看了一半的位置丢了。
原因:这个问题的根源多半不是嵌套Fragment本身,而是子Fragment用了replace替换而不是show/hide。replace每次切换都会销毁旧Fragment的View,滚动位置随之消失。另一种可能是RecyclerView没有设置layoutManager,默认的LinearLayoutManager在View销毁重建后不会自动恢复position。
解决:第一优先用hide/show方案替代replace;第二在RecyclerView上设置setSaveEnabled(true)并且保证LayoutManager存在;第三,如果仍然丢失,在子Fragment的onDestroyView里记下当前位置,onViewCreated里恢复:
private int savedPosition = 0; @Override public void onDestroyView() { super.onDestroyView(); LinearLayoutManager lm = (LinearLayoutManager) recyclerView.getLayoutManager(); if (lm != null) { savedPosition = lm.findFirstVisibleItemPosition(); } } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); recyclerView.getLayoutManager().scrollToPosition(savedPosition); }这样即使View重建,也能回到用户上次看到的位置附近,虽然不是逐像素精确,但用户感受上已经接近无缝了。
6. 进阶:把ViewPager2和childFragmentManager组合成真正的多页承载方案
手写hide/show适合tab数量少且不需要横向滑动的场景。如果tab数量在4个以上,或者产品要求在tab栏上支持左右滑动切换,就需要换成ViewPager2。嵌套Fragment下的ViewPager2有一个关键参数:创建adapter时要传getChildFragmentManager(),而不是getSupportFragmentManager()。这个参数决定了ViewPager2创建的子Fragment归属于外层Fragment的生命周期管理,用错的话,滑动切换时子Fragment在多层嵌套中会失去状态。
进阶代码示例:
public class OuterPagerFragment extends Fragment { private ViewPager2 viewPager; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_outer_pager, container, false); viewPager = root.findViewById(R.id.view_pager); viewPager.setAdapter(new TabPagerAdapter(this)); viewPager.setOffscreenPageLimit(2); // 保留当前页左右各一页 return root; } private static class TabPagerAdapter extends FragmentStateAdapter { public TabPagerAdapter(@NonNull Fragment fragment) { super(fragment); // 传入外层Fragment,内部使用其childFragmentManager } @Override public int getItemCount() { return 3; } @NonNull @Override public Fragment createFragment(int position) { return ChildTabFragment.newInstance(position); } } }FragmentStateAdapter的构造方法第一个参数传Fragment类型,内部会自动调用fragment.getChildFragmentManager(),这是官方为嵌套Fragment提供的标准API。相比直接写ViewPager2 + FragmentPagerAdapter的老方案,FragmentStateAdapter更安全,页面的生命周期也完全由ViewPager2接管,不需要手动写show/hide逻辑。缺点是它内部会缓存若干页面,相比手写方案少了一些控制权,preload的页面数量由offscreenPageLimit控制。
使用ViewPager2时还要留意tab联动。给tab栏和ViewPager2之间建立双向绑定:tab点击时调用viewPager.setCurrentItem(position, true);ViewPager2滑动结束后通过registerOnPageChangeCallback回调更新tab状态。懒加载逻辑也要对应调整:ViewPager2中的页面没有onHiddenChanged,需要在onResume里结合getUserVisibleHint(失效)或者直接用Fragment生命周期切换。实际上ViewPager2里的页面onResume就是可见状态,因为非当前页会被FragmentStateAdapter销毁或保留但不可见。懒加载推荐在onResume中做,同时标记是否已经加载过一次,避免每次切换都重复请求。我自己从手写hide/show切到ViewPager2时,最大的习惯改动就是不再写onHiddenChanged,而是把数据加载统一收敛到onResume加首次加载标记。
最后一个建议:无论用哪种方案,在子Fragment里都别直接用new Fragment()创建页面,统一用带Bundle参数的工厂方法。这能保证配置变更后系统重建Fragment时,参数不会丢。嵌套Fragment的层级越深,状态恢复的复杂度越高,而工厂方法加tag查询这两招能稳定解决大部分状态异常。这套组合我已经连续用了两年多,已经形成肌肉记忆了。希望帮到你。
本文还有配套的精品资源,点击获取