简介:这是一份用于学习Android Fragment嵌套及多Tab页面构建的完整示例工程,面向已具备Activity基础、希望深入掌握Fragment管理与TabLayout/ViewPager组合用法的移动端开发者。项目通过实际案例展示了如何用FragmentPagerAdapter关联多个子Fragment,并借助getChildFragmentManager在父Fragment中管理嵌套页面;内容覆盖子Fragment生命周期、事件回传、页面状态恢复、避免重复创建等常见难点,同时提供了TabLayout与ViewPager绑定、PagerAdapter实现等关键步骤的清晰代码路径。压缩包共330个文件,约10.42MB,以25个Java源码、40个XML布局/配置、175个PNG图片为主,另含65个class、jar、so、apk等编译与运行产物,可对照源码、编译结果和实际界面快速理解每个模块,也可直接安装运行查看效果。已有1201人学习,适合通过阅读源码、修改实践来系统梳理多Tab场景下的Fragment嵌套方案,掌握多层Fragment的事件传递与生命周期管理,并积累常见异常的定位与调试思路。
1. 从实际需求说起:为什么Fragment嵌套Fragment会让很多人头疼
我做Android开发这几年,Fragment嵌套Fragment算是在项目中出现频率非常高的一种场景。尤其是首页那种底部Tab再套顶部Tab的App结构,或者一个订单列表页里再切“全部/待付款/待发货/已完成”这种多状态页面,几乎绕不开嵌套Fragment的思路。
不少刚接触这块的人一开始会直接在一个Fragment的布局里塞一个<fragment>标签,然后调用getSupportFragmentManager()去替换子页面,结果要么白屏,要么状态错乱,要么App直接抛IllegalStateException崩溃。这个坑我自己刚入门时也踩过,所以这篇把嵌套Fragment实现多tab页面的完整思路、底层机制、实操步骤和踩坑记录都整理出来,希望对正在做类似需求的人有帮助。
这篇文章适合这几类人看:
- 正在用Fragment做多tab业务,但切换或重建时出现各种诡异问题的
- 想搞懂
getChildFragmentManager和getSupportFragmentManager到底有什么区别的 - 想要一套稳定、可以直接拿过去改改就能用的嵌套Fragment多tab模板的
- 面试前想彻底梳理Fragment嵌套原理的
内容不堆概念,全都围绕实际代码和运行现象来聊。
2. 整体架构设计:嵌套Fragment多tab应该怎么搭
2.1 设计目标与选型
先明确一下我们要实现的东西。我给它的定位是:外层一个容器Fragment,内部通过左右滑动或点击Tab切换内层子Fragment,每个子Fragment是独立业务页面,体系内数据不共享,但都受外层Fragment生命周期统一管理。
选型上有两条路:
- 一条是
TabLayout + ViewPager2 + FragmentStateAdapter,每个tab页是一个独立Fragment - 另一条是
TabLayout + 手动replace,每次切换时用事务替换外层Fragment里的子Fragment
我实际推荐第一条,原因有二。第一,ViewPager2天然支持左右滑动,符合大多数App多tab页的交互习惯。第二,FragmentStateAdapter会在页面可见性变化时自动做onShow/onHide,配合懒加载非常省心。
手动replace的方式在tab数量少、不需要滑动切换逻辑时可以用,但页面多以后事务管理和生命周期通知会让你变得非常被动。所以下文的方案都以ViewPager2 + FragmentStateAdapter为例展开。
2.2 层级关系与数据隔离
嵌套之后,层级关系变成这样:
Activity └── 外层ContainerFragment(宿主,持有TabLayout和ViewPager2) ├── 子Fragment A ├── 子Fragment B └── 子Fragment C这里最关键的是:内层子Fragment的FragmentManager不再使用Activity的,而是使用外层ContainerFragment的getChildFragmentManager()。
为什么必须这样?因为Fragment内部再挂Fragment,本来就是“子容器”概念。你如果强行把内层Fragment交给Activity的FragmentManager管理,等于让两套生命周期栈之间互相穿插,Activity重建时会发现找不到父Fragment和子Fragment之间的归属关系,轻则状态恢复错乱,重则直接崩溃。
关于这个机制,后面第三节专门拆开讲。
2.3 为什么不用单Activity多Fragment的传统方案硬切
有些场景下不嵌套也能做多tab。比如Activity里放一个FrameLayout,切换tab时replace不同的Fragment。这种做法页面少时问题不大,可一旦tab数量到四五个,而且每个tab内部还有自己的异步加载、列表滚动位置、表单填写状态,replace的代价就高了。
因为replace模式下Fragment每次切换都会走onDestroyView,等切回来时重新onCreateView,这意味着所有UI状态要么重新加载,要么你手动保存恢复,工程量不小。嵌套模式的价值就在这里:内层Fragment一旦被创建并加载,ViewPager2会帮你保留它,切换tab时只是触发onHiddenChanged或setUserVisibleHint这类可见性回调,不会销毁重建。这就是嵌套多tab能达到“切换流畅不闪烁”的根本原因。
3. 核心机制深挖:ChildFragmentManager和FragmentStateAdapter必须搞懂的点
3.1 ChildFragmentManager到底干了什么
getChildFragmentManager()是FragmentManager的一个方法,它返回的是当前Fragment内部专用的FragmentManager实例。换句话说,每一个Fragment都自带一个只能管理自己内部子Fragment的容器。
对比一下:
getSupportFragmentManager():Activity的FragmentManager,管理Activity直接持有的FragmentgetChildFragmentManager():当前Fragment的FragmentManager,管理当前Fragment内部的子FragmentgetParentFragmentManager():获取上层管理者,如果当前Fragment嵌套在另一个Fragment里,返回的是父Fragment的ChildFragmentManager
用生活化的话说,Activity是一个公司,Fragment是部门,ChildFragmentManager是部门经理。部门内部怎么分组、怎么调动,应该部门经理说了算,不能直接报告给公司总裁。如果你把部门内部的事务越过部门经理直接交给公司总裁处理,总裁根本不知道这个人是哪个部门的,工作安排自然就乱套了。
在嵌套Fragment时,内层Fragment的宿主必须通过getChildFragmentManager()获取,否则会报:
Fragment... has not been attached yet或者更常见的是:
java.lang.IllegalStateException: FragmentManager is already executing transactions这类问题,后面实操部分会给出完整示例代码。
3.2 FragmentStateAdapter的内部工作逻辑
ViewPager2搭配FragmentStateAdapter时,adapter需要传入一个关键参数——FragmentManager。
class OrderTabAdapter( fragment: Fragment ) : FragmentStateAdapter(fragment) { // ... }传Fragment而不是FragmentActivity,是因为adapter内部会调用fragment.getChildFragmentManager()。这就保证了adapter创建出来的页面Fragment,全部嵌套在当前Fragment内部,而不是Activity层面。
FragmentStateAdapter还有几个细节值得注意:
第一,它继承自RecyclerView.Adapter,但实际管理Fragment的添加、移除、状态保存全部由FragmentManager完成,adapter本身只负责“当前需要展示哪个Fragment”的决策。
第二,getItemCount()返回的是tab数量,createFragment(int position)返回对应位置的Fragment实例。ViewPager2在滑动过程中,会预加载相邻页面,默认offscreenPageLimit为1。预加载意味着你滑到第二页时,第三页可能已经被创建了。
第三,FragmentStateAdapter内部通过id和tag标识Fragment,脱离adapter本身去手动操作这些Fragment是危险的。如果你想在外部获取当前子Fragment,应该通过supportFragmentManager.findFragmentByTag间接操作,直接持有ViewPager2里的Fragment实例引用很容易引起状态混乱。
3.3 子Fragment的生命周期转移
嵌套结构下,子Fragment的生命周期由父Fragment的ChildFragmentManager控制,而父Fragment本身的生命周期由Activity控制。所以整个链路是:
Activity状态变化 → 父Fragment的onStart/onStop/onDestroyView等 → 子Fragment对应的生命周期回调这意味着,如果你在父Fragment的onDestroyView里做了释放资源的操作,而子Fragment还在使用那些资源,就会有野指针或者空指针风险。实际项目中我习惯把与UI相关的资源释放放在父Fragment的onDestroyView,把数据层和网络层的取消操作放在子Fragment自己的onDestroyView,做好职责划分。
4. 实战:手写一套Fragment嵌套多tab页面的核心代码
4.1 外层容器Fragment的布局
主页面只有一个TabLayout和ViewPager2上下排列。TabLayout用来自定义样式的,看需求来。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <com.google.android.material.tabs.TabLayout android:id="@+id/tabLayout" android:layout_width="match_parent" android:layout_height="wrap_content" app:tabGravity="fill" app:tabMode="fixed" /> <androidx.viewpager2.widget.ViewPager2 android:id="@+id/viewPager" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> </LinearLayout>注意ViewPager2的高度用layout_weight="1",别写死,不然键盘弹起或其他界面变化时容易出现测量问题。
4.2 外层Fragment的Adapter配置
核心代码就这一段:
class ContainerFragment : Fragment(R.layout.fragment_container) { private lateinit var tabLayout: TabLayout private lateinit var viewPager2: ViewPager2 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) tabLayout = view.findViewById(R.id.tabLayout) viewPager2 = view.findViewById(R.id.viewPager) val titles = listOf("全部", "待付款", "待发货", "已完成") val adapter = TabFragmentStateAdapter(this, titles) viewPager2.adapter = adapter // 关键:把TabLayout和ViewPager2联动起来 TabLayoutMediator(tabLayout, viewPager2) { tab, position -> tab.text = titles[position] }.attach() } } class TabFragmentStateAdapter( fragment: Fragment, private val titles: List<String> ) : FragmentStateAdapter(fragment) { override fun getItemCount(): Int = titles.size override fun createFragment(position: Int): Fragment { return when (position) { 0 -> OrderListFragment.newInstance("全部") 1 -> OrderListFragment.newInstance("待付款") 2 -> OrderListFragment.newInstance("待发货") else -> OrderListFragment.newInstance("已完成") } } }这几行代码里面隐含了几个要点:
一是TabFragmentStateAdapter构造方法里传入的fragment,就是ContainerFragment自身。源码里FragmentStateAdapter(fragment: Fragment)最终会调用fragment.getChildFragmentManager()和fragment.getLifecycle(),这两个参数决定了子Fragment的归属和生命周期绑定。你要是误传了Activity,代码跑起来照样能显示,但重建时会有一堆难以追踪的问题。
二是TabLayoutMediator必须调attach(),而且要在adapter设置之后。它的作用不只是设置文字,还会处理TabLayout和ViewPager2之间的选中状态同步。不attach的话,点Tab不会切换页面,滑动页面时Tab也不会高亮。
三是createFragment里的newInstance,这是Fragment的标准写法。不要在构造方法里传参,因为系统重建Fragment时会调用无参构造,自定义参数会丢失。用静态newInstance配合arguments传参,系统恢复状态时才能把参数还原回来。
4.3 内层子Fragment:带懒加载的处理
子Fragment是业务页,通常会请求网络数据。问题在于:ViewPager2默认预加载相邻页,Fragment被创建不代表用户看到了它。如果每次onResume都请求一遍,预加载页面也会触发请求,这就浪费流量了。
常见的解决方案是用setUserVisibleHint来判断用户是否真正看到当前页。对ViewPager2来说,Fragment被切换到可见时会调用setUserVisibleHint(true),不可见时调用setUserVisibleHint(false),但这个方法在Fragment重建时有可能在onCreateView之前调用,所以不能直接在这个方法里操作UI。
一个稳定做法是这样:
class OrderListFragment : Fragment(R.layout.fragment_order_list) { private var isViewCreated = false private var isUserVisible = false companion object { @JvmStatic fun newInstance(type: String): OrderListFragment { return OrderListFragment().apply { arguments = Bundle().apply { putString("type", type) } } } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) isViewCreated = true // 初始化RecyclerView、绑定数据 lazyLoadIfNeeded() } override fun setUserVisibleHint(isVisibleToUser: Boolean) { super.setUserVisibleHint(isVisibleToUser) isUserVisible = isVisibleToUser lazyLoadIfNeeded() } private fun lazyLoadIfNeeded() { if (isViewCreated && isUserVisible) { // 真正加载数据 loadData() // 加载完成后可复位,防止重复loading } } override fun onDestroyView() { super.onDestroyView() isViewCreated = false } }这个做法的核心是双条件判断:View已经创建、当前Fragment对用户可见,两个条件同时满足才去加载数据。
有几个注意事项要补充一下。
第一,从AndroidX Fragment 1.1.0之后,官方不推荐直接依赖setUserVisibleHint做懒加载,因为它可能在onResume之后才回调时序不稳定,但在ViewPager2场景里用的人仍然很多,实测下来是可以工作的。如果你的项目用的是较新版本,你也可以改用onResume配合FragmentPagerAdapter的getItemId机制,不过那套复杂度更高,这里不多展开。
第二,setUserVisibleHint在ViewPager2快速滑动的过程中会被多次触发,所以你要在“请求已发出”这个状态上加个标记,避免重复请求同样的数据。
4.4 更多业务场景下的Fragment传参与通信
子Fragment之间一般不要直接互相持有实例。如果A页签一个按钮,切到B页后需要刷新B页数据,可以用ViewModel做到页面共享,这是官方比较推荐的方式。
具体做法是让外层ContainerFragment和一个子Fragment共享同一个ViewModel,或者所有tab页共享宿主Activity级别的ViewModel。共享的ViewModel生命周期范围要选准:
- 子Fragment各自持有自己的ViewModel,数据跟着子Fragment走
- ContainerFragment和子Fragment共享ViewModel,ContainerFragment销毁时数据清除
- Activity级别的ViewModel,整个Activity存活期间都有效
一般多tab页面的筛选条件、订单状态等共享数据都放在Activity级别。这样每个子Fragment通过activityViewModels()拿到同一个实例,数据同步问题就交给LiveData或StateFlow处理,不需要手动回调FragmentManager。
4.5 另一种选择:外层不用ViewPager,用FragmentTransaction
如果你的tab不是左右滑动那种,只是点击顶部按钮切换,并且每个tab页面非常重,不需要预加载,那么用FragmentTransaction手动切换会更合适。
supportFragmentManager.beginTransaction() .setCustomAnimations(...) .show(targetFragment) .hide(currentFragment) .commit()这里要注意,这里说的supportFragmentManager,是外层ContainerFragment里的childFragmentManager。如果你在ContainerFragment里写,那么:
childFragmentManager.beginTransaction() .show(targetFragment) .hide(currentFragment) .commit()使用show/hide而不是replace,页面切换时不会重新创建,状态保持非常好。缺点是tab页面没有预加载,每次切到新tab时要现加载数据,体验上会略有延迟。
5. 我在嵌套多tab里踩过的坑和排查方法
5.1 崩溃:FragmentManager is already executing transactions
这个错误我在刚把Fragment改造成嵌套结构时遇到过不少次。原因是在Fragment的一次生命周期回调里,连续执行了多个事务,或者在上一个事务尚未提交完成时又试图提交新事务。
常见的触发场景:在onResume里根据某个标志位动态添加子Fragment,又同时调用commitNow(),两个事务叠加就会崩。
规避办法是:
- 尽量使用
commit()而不是commitNow() - 不要在一个生命周期回调里连续提交多个事务
- 如果要依赖事务执行状态做后续操作,用
addOnCommitListener监听完成时机
5.2 页面重建后tab错乱或空白
这是一个大坑。App进程被回收后,系统会根据保存的状态恢复Activity,而Fragment嵌套层级深的时候,原外层Fragment会重建,adapter也会重建,但ViewPager2内部恢复Fragment时是按照tag或id恢复的。如果你在createFragment里每次都new一个Fragment,可能在恢复时找不到对应的状态,出现空白页。
解决办法是给每个Fragment指定稳定的tag或id。FragmentStateAdapter默认会为每个item生成稳定的id,所以一般情况下没问题。但如果你自定义了getItemId(),确保它是稳定且唯一的。我在一个项目里因为根据position生成id,增删tab后旧Fragment恢复错位,后来改成内容hash生成id才解决。
5.3 子Fragment的View重叠
当页面快速切换、多次重建后,偶尔会出现新Fragment的View叠加在老Fragment的View上面。这是因为Fragment事务没有正确移除旧的View,或者FragmentManager的保存状态和实际View状态不一致。
我处理这种问题的经验是:外层容器不要用<fragment>静态标签,因为静态标签在嵌套中管理不可控。尽量用FrameLayout+ 代码事务替换。其次,在销毁Fragment时确认onDestroyView里把根View置空,避免Fragment被复用后带有旧View。
5.4 tab页内嵌别的Fragment导致二次嵌套崩溃
热词里提到的“vant组件dialog嵌套方法”“wpf嵌套winform”,本质上是“嵌套容器”这一类问题的不同形态。Android里如果tab页面内还有弹窗、子页签,继续嵌套Fragment时要注意层级不要过深,一般来说三层以内问题不大,超过三层出问题的概率会显著上升,状态恢复时尤其明显。
如果确实要做很深的多级嵌套,建议每一层宿主Fragment都单独用childFragmentManager,不要越级管理。你在深层子Fragment里要拿到最上层Activity时,用requireActivity(),但你要拿到同级的兄弟Fragment时,需要通过共同的父Fragment去操作,不要直接持有全局FragmentManager。
5.5 Fragment重叠导致的黑屏或闪烁
这种情况大多是因为replace和add混用。我的建议是从一而终:要么全部用show/hide,要么全部用replace,不要同一个容器里一会儿show/hide一会儿replace。一旦混用,FragmentManager的容器记录就会混乱,View的移除和添加顺序就可能不对。
如果用了show/hide,还要设置默认隐藏的Fragment的View为GONE而不是INVISIBLE,否则被隐藏的页面依然会占测量空间。
6. 最后一个实操建议
我在实际使用中发现,嵌套Fragment多tab页面最容易出问题的不是编码期间,而是Activity重建或进程重启之后。所以做这块需求时有个习惯就是反复杀进程验证状态恢复,别只盯着正常流程调通了就完事。
另外,如果业务里面子Fragment很多,每个tab页又包含列表,可以自己封装一个BaseTabFragment,把懒加载、页面埋点、错误重试这些公共逻辑放进基类,子类只需要关心数据和UI绑定。对团队协作来说,这样接口清晰,后面接手的人也不会在生命周期问题上反复踩坑。
以上这套方案在我经手的多个电商类项目里跑得比较稳,如果你也在做Fragment嵌套多tab,可以直接拿里面的代码骨架改业务,先保证跑通,再去优化细节。
本文还有配套的精品资源,点击获取