Fragment 全解
Fragment 不属于四大组件Activity / Service / BroadcastReceiver / ContentProvider它没有自己独立的生命周期入口必须依附在 Activity或另一个 Fragment之中。但它太重要了——现代 Android 主流的单 Activity 架构正是建立在 Fragment 之上。一、Fragment可复用的界面碎片在 Activity 篇 里我们打过一个比方Activity 是相框Fragment 是相框里可以随时替换的照片。Activity 是用户看到的窗口但一个窗口里的内容往往需要拆分和复用手机上是「列表页」和「详情页」两个界面平板上却希望它们并排显示在同一个屏幕里一个底部导航栏 App点击不同 Tab 切换内容但顶部栏、底部栏始终不变。如果一个界面就开一个 Activity上面这些需求会非常难写。Fragment 就是为此而生它是一段可复用的、带有自己生命周期和界面的 UI 单元可以被组合进 Activity也可以在运行时动态添加、替换、移除。一个最简单的 Fragment 长这样classProfileFragment:Fragment(R.layout.fragment_profile){overridefunonViewCreated(view:View,savedInstanceState:Bundle?){super.onViewCreated(view,savedInstanceState)// 在这里拿到布局里的控件绑定数据、设置监听valnameTextview.findViewByIdTextView(R.id.name)nameText.text张三}}注意Fragment不需要在AndroidManifest.xml中注册这点和四大组件完全不同它由宿主 Activity 在代码或布局中加载。Fragment 与 Activity 的关系一个 Activity 可以容纳一个或多个FragmentFragment 的生命周期完全依赖于宿主 Activity——Activity 销毁了里面的 Fragment 也会跟着销毁我们日常用的AppCompatActivity→ 继承自FragmentActivity正是FragmentActivity提供了管理 Fragment 的能力Jetpack 的Navigation组件就是基于单 Activity 多 Fragment架构设计的。注意版本与依赖现在开发应优先使用 AndroidX 的androidx.fragment.app.Fragment而不是已废弃的平台版android.app.Fragment。本文所有内容都基于 AndroidX Fragment。二、单 Activity 架构早期 Android 是一个界面一个 Activity。但随着平板、折叠屏、大屏设备出现这种方式越来越力不从心问题纯 Activity 方案Fragment 方案平板分屏列表 详情并排难以实现Activity 无法并排两个 Fragment 放进同一界面即可界面复用同一块 UI 多处使用复制粘贴布局和逻辑写一个 Fragment 到处用切换内容但保留外壳底部导航频繁启动 Activity外壳重复一个 Activity 壳切换 Fragment页面间转场动画受限于 Activity 转场Fragment 事务可灵活控制动画于是现代应用的主流架构变成了一个 Activity 作为外壳 多个 Fragment 作为内容页页面跳转交给 Navigation 组件管理。这就是单 Activity 架构Single-Activity Architecture也是 Google 目前推荐的做法。三、Fragment生命周期理解 Fragment最核心的就是它的生命周期。它比 Activity 复杂原因在于Fragment 有两条命——它自身的实例Fragment 对象和它的视图View是分开管理的。这一点至关重要后面会专门展开。先看完整的回调顺序。3.1 创建阶段的回调顺序从添加到 Activity到界面显示Fragment 依次经历onAttach()─── Fragment 与宿主 Activity 关联此时可以拿到ContextonCreate()─── 创建 Fragment 实例初始化非视图数据注意此时还没有 ViewonCreateView()─── 创建并返回 Fragment 的视图加载布局onViewCreated()─── 视图已创建完成在这里操作控件最安全onViewStateRestored()─── 恢复视图保存的状态如输入框文字onStart()─── Fragment 可见onResume()─── Fragment 到达前台可与用户交互← 用户正常使用中 →3.2 销毁阶段的回调顺序onPause()─── 失去前台焦点onStop()─── 完全不可见onDestroyView()───销毁视图但 Fragment 实例还在onDestroy()─── 销毁 Fragment 实例onDetach()─── Fragment 与 Activity 解除关联3.3 每个回调该做什么回调什么时候调用你该做什么onAttach()与 Activity 关联时获取Context做与宿主相关的初始化onCreate()创建实例时初始化与视图无关的数据如读取参数argumentsonCreateView()需要绘制界面时加载并返回布局推荐用构造函数传布局 ID 替代手写此方法onViewCreated()视图创建完成后绑定控件、设置监听、观察 ViewModel/LiveDataonStart()即将可见注册 UI 相关监听onResume()到达前台开启动画、请求相机/传感器onPause()即将失去前台暂停动画、提交临时数据onStop()完全不可见释放重量级资源onDestroyView()视图被销毁清理对 View 的引用如置空 binding注销视图相关监听onDestroy()实例被销毁释放剩余资源onDetach()与 Activity 解除关联清理对 Context 的引用注意不要在onCreateView()之前如onCreate()操作视图控件因为那时 View 还不存在。绑定控件、设置点击事件等统一放在onViewCreated()里最安全。四、Fragment 的两条命实例 vs 视图这是 Fragment 生命周期里最容易出 Bug的地方必须单独讲清楚。普通情况下 Fragment 走完整流程没问题。但有一种常见场景会让实例还活着、视图却已经死了把 Fragment 放进BackStack返回栈后又导航到另一个 Fragment。此时旧 Fragment 会执行到onDestroyView()——它的视图被销毁了但 Fragment 对象本身没有销毁还留在返回栈里等待回来。当用户按返回键回到它时会重新执行onCreateView()创建一个全新的视图但onCreate()不会再走一遍。首次进入 onCreate → onCreateView → onViewCreated → ... → onResume 被覆盖 onPause → onStop → onDestroyView ← 视图没了实例还在 返回回来 onCreateView → onViewCreated → ... → onResume ← 重建了新视图这带来两个实际后果4.1 视图引用必须及时清理避免内存泄漏如果你用 ViewBinding 持有了视图引用必须在onDestroyView()中置空否则旧视图无法被回收classProfileFragment:Fragment(R.layout.fragment_profile){privatevar_binding:FragmentProfileBinding?null// 只在视图存在期间有效privatevalbindingget()_binding!!overridefunonViewCreated(view:View,savedInstanceState:Bundle?){super.onViewCreated(view,savedInstanceState)_bindingFragmentProfileBinding.bind(view)binding.name.text张三}overridefunonDestroyView(){super.onDestroyView()_bindingnull// 关键视图销毁了引用也要清掉}}4.2 观察数据要用视图的生命周期观察LiveData时不要用thisFragment 的生命周期而要用viewLifecycleOwner视图的生命周期overridefunonViewCreated(view:View,savedInstanceState:Bundle?){super.onViewCreated(view,savedInstanceState)// 正确示例绑定到视图生命周期viewModel.user.observe(viewLifecycleOwner){user-binding.name.textuser.name}}为什么如果用this当 Fragment 视图被销毁onDestroyView但实例仍存活时观察者不会被移除。等数据更新时回调里访问的却是已被销毁的旧视图轻则内存泄漏重则崩溃。viewLifecycleOwner会随视图一起销毁自动解除观察。五、FragmentManager 与事务Activity 用栈管理多个 Activity类似地FragmentManager负责管理一个宿主里的所有 Fragment。获取方式// 在 Activity 中supportFragmentManager// 在 Fragment 中管理它的子 FragmentchildFragmentManager// 在 Fragment 中获取宿主的 FragmentManagerparentFragmentManager所有对 Fragment 的增删改都通过一个**事务Transaction**完成并且必须commit()supportFragmentManager.commit{setReorderingAllowed(true)// 推荐开启优化生命周期调度replaceProfileFragment(R.id.container)// 替换容器里的 FragmentaddToBackStack(profile)// 加入返回栈按返回键可回退}5.1 add / replace / remove 的区别操作含义注意add()往容器里添加一个 Fragment多次 add 会叠加旧的仍在replace()移除容器里现有 Fragment再添加新的最常用remove()移除指定 Fragmentshow()/hide()隐藏/显示不销毁视图适合需要保留状态的 Tab 切换5.2 返回栈Back Stack调用addToBackStack()后这次事务会被记录到返回栈。用户按返回键时系统会回退这次事务撤销 replace恢复上一个 Fragment而不是直接退出 Activity。这正是 Fragment 能模拟页面跳转的关键。提示直接手写FragmentManager事务在简单场景没问题但页面多、跳转复杂时强烈建议改用Navigation 组件见第七节它把这些事务封装成了可视化的导航图。六、Fragment 通信Fragment 应该是自包含、可复用的所以它不应该直接持有另一个 Fragment 或 Activity 的引用去调用对方方法那样耦合太紧复用就破坏了。Jetpack 推荐了几种解耦的通信方式。6.1 共享 ViewModel同一 Activity 内最推荐同一个 Activity 下的多个 Fragment可以共享作用域为 Activity 的 ViewModel通过它间接通信// 两个 Fragment 都这样获取拿到的是同一个 ViewModel 实例privatevalsharedViewModel:SharedViewModelbyactivityViewModels()// A Fragment 写入sharedViewModel.select(item)// B Fragment 观察sharedViewModel.selectedItem.observe(viewLifecycleOwner){item-// 收到 A 的选择}这是平板列表 详情场景的标准做法列表 Fragment 选中某项详情 Fragment 自动更新。6.2 Fragment Result API一次性结果传递如果只是 A → B 传一个结果类似 Activity Result用setFragmentResult/setFragmentResultListener// 接收方在 onCreate / onViewCreated 中注册监听setFragmentResultListener(requestKey){_,bundle-valresultbundle.getString(data)}// 发送方setFragmentResult(requestKey,bundleOf(datatohello))6.3 与宿主 Activity 通信Fragment 需要通知 Activity 时优先用共享 ViewModel轻量场景也可以让 Activity 实现一个接口Fragment 在onAttach()时拿到回调对象。但接口回调方式耦合较高新代码更推荐 ViewModel 方案。七、Navigation 组件手写事务和返回栈在复杂应用里容易出错。JetpackNavigation把这些封装成了一张可视化的导航图Nav GraphNavGraph一张 XML或 Compose DSL图描述有哪些目的地Fragment以及它们之间怎么跳NavHostFragment放在 Activity 布局里的容器所有 Fragment 在此显示NavController负责执行跳转、管理返回栈。// 在 Fragment 中跳转到详情页并安全地传参findNavController().navigate(HomeFragmentDirections.actionHomeToDetail(itemId42))Navigation 的优势类型安全传参Safe Args 插件编译期检查参数自动处理返回栈和返回键统一管理转场动画、深层链接Deep Link与底部导航栏、抽屉菜单一键集成。现状提示在 Jetpack Compose 中导航的主体已逐渐变成Navigation Compose直接在可组合函数之间导航不再需要 Fragment。但只要项目还使用传统 View 体系或 Fragment“Navigation Fragment” 仍是主流且完全推荐的方案。八、几种特殊的 Fragment类型用途DialogFragment用 Fragment 的方式实现对话框能正确处理屏幕旋转推荐替代直接 new 一个DialogBottomSheetDialogFragment从底部弹出的面板来自 Material 库PreferenceFragmentCompat快速搭建设置页配合 XML 自动生成设置项NavHostFragmentNavigation 组件的容器 Fragment参考来源Android 官方文档 - FragmentsFragment 生命周期在 Fragment 之间通信Navigation 组件