一文读懂Android Fragment栈管理

导读

作为 Android 四大组件之一的 Fragment,其使用频率是相当高的,但是想要管理好 Fragment 栈也不是一件容易的事。下面我们就来看看 Fragment、FragmentManager、事务、操作、回退栈、容器之间的关系。

基本概念

Fragment

什么是Fragment?

  • Fragment 是 Android 在 Activity 内部的一个"子模块" ,也被称为 片段
  • 它既有 自己的布局(UI) ,也有 自己的生命周期,但必须由 activity 或其他 fragment 托管。
  • 可以把它理解为:Activity 是一个大容器,Fragment 是可以复用的小页面组件

为什么要用 Fragment?

  1. 模块化:可以把一个复杂界面拆分成多个小的可复用模块。
  2. 适配性:在手机、平板等不同屏幕上,可以灵活组合 Fragment。
  3. 可复用性:同一个 Fragment 可以在不同的 Activity 中复用。
  4. 导航管理:通过 Fragment 栈,可以方便地实现页面切换和返回。

Fragment 的生命周期

和 Activity 类似,但更细化:

  • onAttach → onCreate → onCreateView → onActivityCreated → onStart → onResume
  • onPause → onStop → onDestroyView → onDestroy → onDetach

创建Fragment

因为Fragment要寄托在Activity中,所以先建一个Activity:

点击包名→鼠标右键→NewActivityEmpty Views Activity

如果一开始没有Activity,记得勾选为Launcher Activity,不然运行会看不到应用。

然后新建Fragment,包名右键→NewFragmentFragment(Blank)

随便取个名字,这里取为 Fragment1

同步会生成一个Fragment_1.xml的布局,activity_main.xml是刚刚新建Activity产生的:

现在要把 Fragment1 放到 MainActivity 中:

  1. 修改activity_main.xml
xml 复制代码
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <!-- 加入容器 -->
    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/fragment_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
  1. 修改MainActivity.kt
kotlin 复制代码
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // 添加Fragment1到容器中
        if (savedInstanceState == null) {
            supportFragmentManager.beginTransaction()
                .replace(R.id.fragment_container, Fragment1())
                .commit()
        }
    }
}

这样,Activity 中就能显示 Fragment1 的内容了。:

容器(FragmentContainerView

容器就是activity_main.xml里面的FragmentContainerView

  • FragmentContainerView 是 Android 官方在 Fragment 1.2.0 中引入的一个容器控件。
  • 它的作用是:专门用来作为 Fragment 的容器 ,推荐替代过去常用的 FrameLayout

为什么要有 FragmentContainerView

在以前,大家常用 FrameLayout 作为 Fragment 的占位容器:

xml 复制代码
<FrameLayout
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

这种方式有几个问题:

  1. 不直观FrameLayout 本身不是为 Fragment 准备的。
  2. 限制较多 :有些场景下(比如 XML 里直接用 <fragment> 标签),Fragment 的行为难以控制。
  3. FragmentTransaction 的一些特性支持不好

为了解决这些问题,Google 推出了 FragmentContainerView。所以现在的Fragment最好都使用FragmentContainerView作为容器。

一句话总结

FragmentContainerView 是专门为 Fragment 提供的容器控件,推荐替代 FrameLayout 使用,能更好地支持 Fragment 的生命周期和事务管理。

宿主

Activity 或 Fragment 都可以是宿主,只要里面带有容器。

Fragment管理器(FragmentManager)

什么是FragmentManager?

  • FragmentManager 是 Android 提供的 Fragment 管理器 ,用来在 Activity 或 Fragment 内部动态管理 Fragment
  • 它的职责是:添加、移除、替换、查找 Fragment ,以及 管理 Fragment 回退栈

FragmentManager的几种常见获取方式

  1. 在 Activity 中获取 FragmentManager
kotlin 复制代码
// 在 Activity 中
val fragmentManager = supportFragmentManager
// 如果是没有继承 AppCompatActivity 的原生 Activity,可以写:
val fragmentManager = fragmentManager
  1. 在 Fragment 中获取 FragmentManager
kotlin 复制代码
// 获取父级 FragmentManager(通常用来操作 Activity 或父 Fragment 中的 Fragment)
val parentManager = parentFragmentManager

// 获取子 FragmentManager(用于管理当前 Fragment 内部的子 Fragment)
val childManager = childFragmentManager
  1. 在任意 Fragment 内部获取 Activity 的 FragmentManager
kotlin 复制代码
val fragmentManager = requireActivity().supportFragmentManager

FragmentManager和Fragment的关系

为了更好的了解 FragmentFragmentManager 之间的关系,我们再创建一个新的Fragment2,放到Fragment1里面:

  1. 修改fragment_1.xml,加入Fragment容器,顺便美化一下页面
xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical"
  tools:context=".Fragment1">

  <TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Fragment1 内容"
    android:textSize="18sp"
    android:textStyle="bold"
    android:padding="16dp"
    android:background="#E3F2FD"
    android:gravity="center" />

  <androidx.fragment.app.FragmentContainerView
    android:id="@+id/fragment2_container"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1" />

</LinearLayout>

页面效果如下:

  1. 修改fragment_2.xml
xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="#FFF3E0"
    tools:context=".Fragment2">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Fragment2 内容"
        android:textSize="16sp"
        android:textStyle="bold"
        android:padding="12dp"
        android:background="#FF9800"
        android:textColor="@android:color/white"
        android:gravity="center" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:text="这是嵌套在Fragment1内部的Fragment2"
        android:textSize="14sp"
        android:padding="16dp"
        android:gravity="center"
        android:layout_margin="8dp"
        android:background="@android:color/white"
        android:elevation="2dp" />

</LinearLayout>

页面效果如下:

  1. Fragment2放到Fragment1的容器中,修改Fragment1.kt
kotlin 复制代码
class Fragment1 : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        val view = inflater.inflate(R.layout.fragment_1, container, false)
        
        // 添加Fragment2到容器中
        if (savedInstanceState == null) {
            childFragmentManager.beginTransaction()
                .replace(R.id.fragment2_container, Fragment2())
                .commit()
        }
        
        return view
    }

    companion object {
        @JvmStatic
        fun newInstance(param1: String, param2: String) =
            Fragment1()
    }
}

最后的显示效果如下:

现在打印一下不同地方的FragmentManager

kotlin 复制代码
// MainActivity.kt 中
Log.d(
    "FragmentManager",
    "MainActivity 中的 supportFragmentManager: $supportFragmentManager",
)
kotlin 复制代码
// Fragment1.kt 中
 Log.d(
    "FragmentManager",
    "Fragment1 中的 childFragmentManager: $childFragmentManager",
)
Log.d(
    "FragmentManager",
    "Fragment1 中的 parentFragmentManager: $parentFragmentManager",
)
Log.d(
    "FragmentManager",
    "Fragment1 中获取 Activity 的 Fragment: ${requireActivity().supportFragmentManager}",
)
kotlin 复制代码
// Fragment2.kt 中
Log.d(
    "FragmentManager",
    "Fragment2 中的 parentFragmentManager: $parentFragmentManager",
)
Log.d(
    "FragmentManager",
    "Fragment2 中获取 Activity 的 Fragment 方式1: ${requireActivity().supportFragmentManager}",
)
Log.d(
    "FragmentManager",
    "Fragment2 中获取 Activity 的 Fragment 方式2: ${parentFragment?.parentFragmentManager}",
)

打印内容如下:

关系图如下

  • 需要引用的相应 FragmentManager 属性取决于调用点在 fragment 层次结构中的位置,以及你尝试访问的 fragment 管理器。

FragmentManager,容器和宿主的关系

  • 一个宿主只会有一个FragmentManager
  • 一个FragmentManager可以管理多个容器

FragmentManager层级视图如下:

scss 复制代码
Activity
 └── supportFragmentManager (管理 Activity 下的所有 Fragment)
      ├── FragmentA
      │     └── childFragmentManager (管理 FragmentA 的子 Fragment)
      │           ├── SubFragmentA1
      │           └── SubFragmentA2
      │
      └── FragmentB
            └── childFragmentManager (管理 FragmentB 的子 Fragment)
                  └── SubFragmentB1

事务(FragmentTransaction)

什么是 FragmentTransaction

  • FragmentTransaction 是由 FragmentManager 创建的,用来 执行一组 Fragment 的增删改查操作 的事务对象。
  • 它保证这些操作要么一次性全部生效,要么都不执行,类似数据库的"事务"。
  • 常见操作包括:add()replace()remove()hide()show()addToBackStack() 等。

常见用法

kotlin 复制代码
val transaction = supportFragmentManager.beginTransaction()

transaction.add(R.id.container, MyFragment(), "MyTag")   // 添加
transaction.replace(R.id.container, AnotherFragment())   // 替换
transaction.addToBackStack(null)                        // 入回退栈
transaction.commit()                                    // 提交事务

提交方式

事务的提交方式分好几种,各有差异:

  1. commit()
    • 异步提交,放入主线程消息队列稍后执行
    • 正常提交事务,必须在 onSaveInstanceState() 之前调用。
    • 否则可能抛出异常(因为 Activity 状态已经保存)。
  1. commitAllowingStateLoss()
    • 异步提交,放入主线程消息队列稍后执行
    • 即使在 onSaveInstanceState() 之后调用也能提交,不会崩溃。
    • 但可能导致 Fragment 状态丢失。
  1. commitNow() / commitNowAllowingStateLoss()
    • 立即执行事务(同步),而不是排队等待主线程调度。
    • 适合对结果有强依赖的场景,但一般不推荐频繁使用。

FragmentManager 与 FragmentTransaction 的关系

  • FragmentManager:管理器,负责创建和调度 Fragment。
  • FragmentTransaction:事务,描述并执行对 Fragment 的具体操作。

👉 类比:FragmentManager 像银行,FragmentTransaction 像一笔转账操作

常见问题

  1. 为什么 Fragment 操作需要事务机制?

为了保证一组 Fragment 操作的原子性和一致性,避免部分操作成功、部分失败,导致 UI 状态混乱。

  1. FragmentTransaction 提交时为什么可能会报 IllegalStateException?

因为事务提交时机晚于 onSaveInstanceState(),状态已经保存,再修改会不一致。

  1. commitAllowingStateLoss() 在什么场景下可用?

比如页面退出时要立即提交事务,但即使丢失状态也无所谓。

操作

FragmentTransaction 的操作,就是指开发者在事务中定义的对 Fragment 的具体动作。

添加 Fragment

将 Fragment 添加到指定容器中。 kotlin

scss 复制代码
supportFragmentManager.beginTransaction()
    .add(R.id.container, MyFragment(), "MyTag")
    .commit()

注意:如果同一个容器只做 add 操作加入多个 Fragment,会导致页面重叠

移除 Fragment

会先移除容器里的 Fragment,再添加新的。

kotlin 复制代码
supportFragmentManager.beginTransaction()
    .replace(R.id.container, AnotherFragment())
    .commit()

替换 Fragment

会先移除容器里的 Fragment,再添加新的。

kotlin 复制代码
supportFragmentManager.beginTransaction()
    .replace(R.id.container, AnotherFragment())
    .commit()
  • replace() 的源码本质上就是:
    1. 遍历容器里已有的 Fragment ,执行 remove()
    2. 再执行一次 add() ,把新的 Fragment 放进容器。
  • 所以 replace() = remove(all) + add(new)

隐藏 / 显示 Fragment

适合在多个 Fragment 间切换时使用(比如底部导航)。

kotlin 复制代码
supportFragmentManager.beginTransaction()
    .hide(myFragment)
    .show(anotherFragment)
    .commit()

注意:

  1. 如果退出后不在现实的Fragment,要使用remove(),不然会一直存在内存中。
  2. 如果事先通过 add()replace() 加入容器的 Fragment,直接调用 show() 是不会生效的。

加入回退栈

让 Fragment 操作支持返回功能。

kotlin 复制代码
supportFragmentManager.beginTransaction()
    .replace(R.id.container, AnotherFragment())
    .addToBackStack(null) // 按返回键可回退
    .commit()

设置过渡动画

常用于切换页面时增加过渡效果。

kotlin 复制代码
supportFragmentManager.beginTransaction()
    .setCustomAnimations(
        R.anim.slide_in_right,  // enter
        R.anim.slide_out_left,  // exit
        R.anim.slide_in_left,   // popEnter
        R.anim.slide_out_right  // popExit
    )
    .replace(R.id.container, AnotherFragment())
    .addToBackStack(null)
    .commit()

回退栈

什么是 Fragment 回退栈?

  • 回退栈是 FragmentManager 提供的机制,用于记录 Fragment 的事务(Transaction)。
  • 当调用 addToBackStack() 时,事务会被保存到回退栈里。
  • 用户按 返回键 或调用 popBackStack() 时,FragmentManager 会把最近的事务回滚,恢复到之前的状态。

常见操作

  1. 加入回退栈
kotlin 复制代码
supportFragmentManager.beginTransaction()
    .replace(R.id.container, FragmentB())
    .addToBackStack(null) // 把事务加入回退栈
    .commit()

按返回键时,会回退到前一个 Fragment。

  1. 手动出栈
kotlin 复制代码
supportFragmentManager.popBackStack()
  1. 清空整个回退栈
kotlin 复制代码
supportFragmentManager.popBackStack(
    null,
    FragmentManager.POP_BACK_STACK_INCLUSIVE
)

注意点

  1. 如果没有调用 addToBackStack()
    • 按返回键不会回到上一个 Fragment,而是直接退出 Activity。
  1. 回退栈存储的是 事务(Transaction) ,而不是单个 Fragment。
    • 比如一个事务里 add(FragmentA) hide(FragmentB),回退时会一起撤销。
  1. 每个 FragmentManager 都有一个回退栈
    • 每个 FragmentManager(通常对应一个 Activity 或 Fragment)维护自己的回退栈。
  1. 返回键默认只作用于 Activity 的回退栈
    • 多个回退栈需要开发者自己协调,默认返回键只管理 Activity 的回退栈。常见做法是 优先消费子 Fragment 的回退栈,再退到 Activity 栈,最后退出。
  1. 不建议加入回退栈和没加入回退栈的事务混用。
    • 比如如下这种情况,最后会出现FragmentAFragmentB重叠显示的问题。
kotlin 复制代码
// 事务1
add(FragmentA) // 显示FragmentA
commit()

// 事务2
hide(FragmentA) // 隐藏FragmentA
addToBackStack() // 加入返回栈
commit()

// 事务3
add(FragmentB) // 显示FragmentB
commit

// 回退
popBackStack() // 回退事务2

生命周期演示

现在我们来看一下执行不同的操作,Fragment的生命周期状态是怎么变化的。

新建一个Fragment3,和Fragment1同级,用于切换的时候查看生命周期变化。再加入两个Fragment相关的控制逻辑。

层次结构如下图:

复制代码
MainActivity
├── Fragment1
│   └── Fragment2
└── Fragment3

页面内容如下:

不带返回栈的事务操作

  1. add Fragment1

和直接调用replace触发的生命周期一样

  1. remove Fragment1
  1. replace Fragment1hide Fragment1show Fragment1

hideshow不会销毁视图,变化只能通过onHiddenChanged方法监听

  1. add Fragment1 -> replace Fragment3

带返回栈的事务操作

修改MainActivity,将操作都加到返回栈中。修改后页面如下:

  1. 执行 ADD F1REMOVE F1POP

可以看到这里和没有加入回退栈的remove事务相比,不会执行onDestroy方法:

这里注意,嵌套的Fragment2会经历先销毁再创建的过程:

这里可以通过先判断Fragment2是否存在,再通过判断要不要创建来解决:

kotlin 复制代码
override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    Log.d(TAG, "onCreateView() - Fragment1 创建视图")
    
    val view = inflater.inflate(R.layout.fragment_1, container, false)

    // 检查Fragment2是否已经存在
    val existingFragment2 = childFragmentManager.findFragmentById(R.id.fragment2_container)
    if (existingFragment2 == null) {
        childFragmentManager.beginTransaction()
            .replace(R.id.fragment2_container, Fragment2.newInstance())
            .commit()
    }
    
    return view
}

其他的操作就不一一演示了,大家可以在文章末尾找到对应的demo仓库地址,自己操作一遍理解会更加深刻

提示:通过logcat查看日志的时候,可以通过tag:Fragment1_Lifecycle tag:Fragment2_Lifecycle tag:Fragment3_Lifecycle tag:MainActivity_FragmentControl 过滤内容。

后记

如果自己管理 Fragment 栈,需要掌握的知识点很多,并且也容易踩坑。好处是能够精细化的控制,做性能优化。

官方推荐使用 Jetpack Navigation 路由组件,相对手动管理,用路由组件的方式更适合大型项目,能减少很多样板代码。

大家有什么好用的路由库,也可以在评论区留言交流。

参考

相关推荐
Aoda2 小时前
浏览器字体设置引发的Bug:从一次调查到前端字体策略的深度思考
前端·css
朝与暮2 小时前
《javascript进阶-类(class):构造函数的语法糖》
前端·javascript
入秋2 小时前
Three.js 实战之电子围栏可根据模型自动生成
前端·前端框架·three.js
用户6120414922132 小时前
jsp+servlet做的咖啡品牌管理后台
java·前端·后端
Asort2 小时前
JavaScript设计模式(三)——抽象工厂模式 (Abstract Factory)
前端·javascript·设计模式
带娃的IT创业者2 小时前
从零构建智能HTML转Markdown转换器:Python GUI开发实战
android·python·html
nyf_unknown3 小时前
(vue)前端下载本地excel文件
前端·vue.js·excel
fcm193 小时前
(6) tauri之前端框架性能对比
前端·javascript·rust·前端框架·vue·react
今晚务必早点睡3 小时前
前端缓存好还是后端缓存好?缓存方案实例直接用
前端·后端·缓存