掌握 Fragment 用法,实现平板适配、底部导航等灵活 UI 架构
哈喽,各位坚持学习的小伙伴们!上一篇我们掌握了 Service 与 BroadcastReceiver,让 App 拥有了后台执行与全局监听的能力。今天,我们回到 UI 层,学习一个让界面架构「活」起来的核心组件 —— Fragment。
很多新手觉得 Fragment 难学,生命周期绕、通信麻烦,但这恰恰是 Fragment 强大的地方。学会了它,你将能轻松实现:
一套代码同时适配手机和平板
底部导航栏 + 多页面切换,页面状态完美保留
页面内的模块化,一个 Activity 拼出多个可复用的 UI 单元
全程附可运行 Kotlin 代码 + 避坑指南,我们直接开始!
一、Fragment 是什么?为什么需要它?
1.1 Fragment 的定位
Fragment 是嵌入在 Activity 中的「模块化 UI 碎片」,拥有独立的布局、生命周期和事件处理能力,可以像搭积木一样在宿主中自由组合与复用。
如果没有 Fragment,所有界面逻辑都塞在一个 Activity 里,代码会极度臃肿、耦合严重,而且在不同屏幕尺寸的设备上几乎无法复用。Fragment 完美解决了这一痛点。
1.2 Fragment 的核心价值
① UI 模块化与复用
多个页面都能使用同一个 UI 模块(例如搜索栏、评论列表),把模块封装成 Fragment,一次编写,处处引用。
② 灵活的页面切换
不需要新建 Activity,通过事务(Transaction)就能完成页面「局部替换」,动画更流畅,性能开销更低。
③ 大屏与平板适配
手机上可能列表和详情各占一个页面,平板上则可以左列表、右详情同时显示。Fragment 让一套代码适配多种屏幕尺寸成为可能。
手机: [ Activity ] → 先显示 ListFragment,点击后替换为 DetailFragment
平板: [ Activity ] → 左侧 ListFragment + 右侧 DetailFragment 同时存在
④ 架构解耦,代码清爽
把一个复杂界面拆分成多个 Fragment,每个 Fragment 专注一小块逻辑,Activity 只当「舞台调度」,代码分层清晰,易于维护。
二、Fragment 的两种加载方式
2.1 静态加载:在 XML 中直接声明
在 Activity 布局文件中通过 标签引入,Fragment 随 Activity 一起初始化和销毁。
<span><<span>fragment</span>
<span>android:id</span>=<span>"@+id/fragment_static"</span>
<span>android:name</span>=<span>"com.example.HomeFragment"</span>
<span>android:layout_width</span>=<span>"match_parent"</span>
<span>android:layout_height</span>=<span>"match_parent"</span> /></span>
适用场景:页面结构完全固定、无需运行时切换的静态模块。 局限:不支持动态替换和移除,灵活性极差,实际项目中很少作为主力用法。
2.2 动态加载:FragmentTransaction 事务(企业主流)
通过代码获取 FragmentManager,开启事务,执行添加、替换、移除、隐藏等操作。这是企业开发的标准方式。
核心 API 一览:
| 方法 | 作用 |
|---|---|
| add(containerId, fragment) | 将 Fragment 添加到容器 |
| replace(containerId, fragment) | 替换容器中现有 Fragment |
| remove(fragment) | 移除 Fragment |
| hide(fragment) / show(fragment) | 隐藏/显示,视图不重建 |
| addToBackStack(name) | 加入返回栈,点返回键可撤销事务 |
标准动态添加模板:
<span>// 仅在 Activity 首次创建时添加,防止屏幕旋转重建导致重叠</span>
<span>if</span> (savedInstanceState == <span>null</span>) {
supportFragmentManager.beginTransaction()
.add(R.id.fragment_container, HomeFragment())
.commit()
}
开发规范:企业开发统一使用动态加载,静态加载只用于极简单的固定模块展示。
三、Fragment 的完整生命周期
Fragment 生命周期比 Activity 更细化,而且完全依附于宿主 Activity。理解生命周期是解决 Fragment 空指针、内存泄漏、页面重叠的关键。
3.1 生命周期流程图
onAttach() ← 与 Activity 绑定
↓
onCreate() ← 初始化非视图数据
↓
onCreateView() ← 膨胀布局,返回根视图
↓
onViewCreated() ← 视图创建完毕,可初始化控件
↓
onStart() ← 可见,但尚未交互
↓
onResume() ← 可交互
↓
onPause() / onStop()
↓
onDestroyView() ← 视图销毁,但 Fragment 实例可能仍在
↓
onDestroy() ← 实例销毁
↓
onDetach() ← 与 Activity 解绑
3.2 关键回调与 Activity 联动
- onAttach(context):Fragment 与宿主 Activity 关联,可在此获取宿主引用。
- onCreateView → onViewCreated:视图初始化的核心区间,推荐在 onViewCreated 中绑定控件、设置监听。
- onDestroyView:视图销毁时回调,须释放所有视图引用,防止内存泄漏。
- 联动规律:Activity onStart → Fragment onStart;Activity onStop → Fragment onStop;Activity 销毁则 Fragment 彻底销毁。
四、Fragment 与 Activity 的通信(正误写法对比)
4.1 ❌ 错误示范:直接强转 Activity 或访问控件
<span>// 在 Fragment 中直接强转宿主 Activity(极不推荐)</span>
<span>val</span> activity = activity <span>as</span> MainActivity
activity.updateTitle(<span>"标题"</span>)
<span>// 在 Activity 中直接操作 Fragment 内部控件</span>
<span>val</span> fragment = supportFragmentManager.findFragmentById(R.id.container) <span>as</span> MyFragment
fragment.textView?.text = <span>"Hello"</span>
致命问题:
- Fragment 与宿主紧耦合,无法复用。
- 当 Fragment 处于后台、宿主被回收时,getActivity() 或 activity 返回 null,直接触发空指针崩溃。
4.2 ✅ 正确姿势一:接口回调(经典轻量方案)
适用于简单少量数据通信,通过接口解耦。
1. 定义通信接口
<span>interface</span> <span>FragmentCallback</span> {
<span><span>fun</span> <span>onDataReceived</span><span>(<span>data</span>: <span>String</span>)</span></span>
}
2. Activity 实现接口
<span>class</span> <span>MainActivity</span> : <span>AppCompatActivity</span>(), FragmentCallback {
<span>override</span> <span><span>fun</span> <span>onDataReceived</span><span>(<span>data</span>: <span>String</span>)</span></span> {
Log.d(<span>"Fragment通信"</span>, <span>"收到:<span>$data</span>"</span>)
}
}
3. Fragment 通过接口回调传递数据
<span>class</span> <span>MyFragment</span> : <span>Fragment</span>() {
<span>private</span> <span>var</span> callback: FragmentCallback? = <span>null</span>
<span>override</span> <span><span>fun</span> <span>onAttach</span><span>(context: <span>Context</span>)</span></span> {
<span>super</span>.onAttach(context)
callback = context <span>as</span>? FragmentCallback
}
<span>private</span> <span><span>fun</span> <span>sendMessage</span><span>()</span></span> {
callback?.onDataReceived(<span>"来自 Fragment 的数据"</span>)
}
<span>override</span> <span><span>fun</span> <span>onDetach</span><span>()</span></span> {
<span>super</span>.onDetach()
callback = <span>null</span> <span>// 释放引用</span>
}
}
4.3 ✅ 正确姿势二:共享 ViewModel(官方推荐,企业主流)
适用于复杂页面、多个 Fragment 共享同一组数据。通过宿主 Activity 范围内的 ViewModel 进行通信,完全解耦,生命周期安全。
<span>// Activity 内共享的 ViewModel</span>
<span>class</span> <span>SharedViewModel</span> : <span>ViewModel</span>() {
<span>val</span> selectedItem = MutableLiveData<String>()
}
<span>// Fragment A:发送数据</span>
<span>class</span> <span>SenderFragment</span> : <span>Fragment</span>() {
<span>private</span> <span>val</span> viewModel: SharedViewModel <span>by</span> activityViewModels()
<span>override</span> <span><span>fun</span> <span>onViewCreated</span><span>(view: <span>View</span>, savedInstanceState: <span>Bundle</span>?)</span></span> {
<span>super</span>.onViewCreated(view, savedInstanceState)
view.findViewById<Button>(R.id.btn).setOnClickListener {
viewModel.selectedItem.value = <span>"Hello from Fragment A"</span>
}
}
}
<span>// Fragment B:接收数据</span>
<span>class</span> <span>ReceiverFragment</span> : <span>Fragment</span>() {
<span>private</span> <span>val</span> viewModel: SharedViewModel <span>by</span> activityViewModels()
<span>override</span> <span><span>fun</span> <span>onViewCreated</span><span>(view: <span>View</span>, savedInstanceState: <span>Bundle</span>?)</span></span> {
<span>super</span>.onViewCreated(view, savedInstanceState)
viewModel.selectedItem.observe(viewLifecycleOwner) { <span>data</span> ->
view.findViewById<TextView>(R.id.tv).text = <span>data</span>
}
}
}
优势:Fragment 之间互相不感知,无接口依赖,数据变更自动驱动 UI,且绑定 viewLifecycleOwner 可避免视图销毁后的内存泄漏。
五、新手必踩坑点清单
坑点 ①:异步回调中 activity 或 getActivity() 返回 null
触发场景:Fragment 发起网络请求,响应返回时用户已离开页面,activity 为 null,直接操作 UI 即崩溃。
解决方案:
- 使用 ViewModel + LiveData,通过 viewLifecycleOwner 观察数据,确保视图存活时才回调。
- 若不得已直接使用 activity,回调前增加 isAdded 判空。
坑点 ②:屏幕旋转 / 重建导致 Fragment 重叠
问题根源:Activity 因屏幕旋转被销毁并重建,系统会自动恢复之前存在的 Fragment,而代码又在 onCreate 中重复添加了一份,导致两个 Fragment 重叠。
解决方案:在添加 Fragment 前判断 savedInstanceState == null,仅在首次创建时执行添加事务。后续交由系统自动恢复。
六、综合案例:新闻分类底部导航(BottomNavigationView + Fragment)
综合运用所学,实现一个经典的新闻分类界面:
- 底部三个 Tab:要闻、科技、娱乐
- 每个 Tab 对应一个 NewsFragment
- 切换时保留页面状态不重建(滚动位置、输入文本不丢失)
- 解决屏幕旋转造成的 Fragment 重叠问题
6.1 添加依赖与菜单资源
app/build.gradle.kts 中添加 Material 库(一般已有):
implementation(<span>"com.google.android.material:material:1.11.0"</span>)
创建底部菜单 res/menu/bottom_nav_menu.xml:
<span><<span>menu</span> <span>xmlns:android</span>=<span>"http://schemas.android.com/apk/res/android"</span>></span>
<span><<span>item</span> <span>android:id</span>=<span>"@+id/nav_news"</span> <span>android:title</span>=<span>"要闻"</span> /></span>
<span><<span>item</span> <span>android:id</span>=<span>"@+id/nav_tech"</span> <span>android:title</span>=<span>"科技"</span> /></span>
<span><<span>item</span> <span>android:id</span>=<span>"@+id/nav_entertainment"</span> <span>android:title</span>=<span>"娱乐"</span> /></span>
<span></<span>menu</span>></span>
6.2 编写 NewsFragment
通过工厂方法传递分类参数,构建通用的新闻页面。
<span>class</span> <span>NewsFragment</span> : <span>Fragment</span>() {
<span>companion</span> <span>object</span> {
<span>private</span> <span>const</span> <span>val</span> ARG_CATEGORY = <span>"category"</span>
<span><span>fun</span> <span>newInstance</span><span>(category: <span>String</span>)</span></span> = NewsFragment().apply {
arguments = Bundle().apply { putString(ARG_CATEGORY, category) }
}
}
<span>override</span> <span><span>fun</span> <span>onCreateView</span><span>(inflater: <span>LayoutInflater</span>, container: <span>ViewGroup</span>?, savedInstanceState: <span>Bundle</span>?)</span></span>: View {
<span>return</span> inflater.inflate(R.layout.fragment_news, container, <span>false</span>)
}
<span>override</span> <span><span>fun</span> <span>onViewCreated</span><span>(view: <span>View</span>, savedInstanceState: <span>Bundle</span>?)</span></span> {
<span>super</span>.onViewCreated(view, savedInstanceState)
<span>val</span> category = arguments?.getString(ARG_CATEGORY) ?: <span>"要闻"</span>
view.findViewById<TextView>(R.id.tv_title).text = <span>"<span>$category</span> 新闻"</span>
}
}
6.3 Activity 布局
<span><<span>androidx.constraintlayout.widget.ConstraintLayout</span> <span>...</span>></span>
<span><<span>androidx.fragment.app.FragmentContainerView</span>
<span>android:id</span>=<span>"@+id/fragment_container"</span>
<span>android:layout_width</span>=<span>"match_parent"</span>
<span>android:layout_height</span>=<span>"0dp"</span>
<span>app:layout_constraintTop_toTopOf</span>=<span>"parent"</span>
<span>app:layout_constraintBottom_toTopOf</span>=<span>"@id/bottom_navigation"</span> /></span>
<span><<span>com.google.android.material.bottomnavigation.BottomNavigationView</span>
<span>android:id</span>=<span>"@+id/bottom_navigation"</span>
<span>android:layout_width</span>=<span>"match_parent"</span>
<span>android:layout_height</span>=<span>"wrap_content"</span>
<span>app:layout_constraintBottom_toBottomOf</span>=<span>"parent"</span>
<span>app:menu</span>=<span>"@menu/bottom_nav_menu"</span> /></span>
<span></<span>androidx.constraintlayout.widget.ConstraintLayout</span>></span>
6.4 核心逻辑:hide/show 切换 + 防重叠
<span>class</span> <span>MainActivity</span> : <span>AppCompatActivity</span>() {
<span>private</span> <span>val</span> fragmentMap = mutableMapOf<<span>Int</span>, Fragment>()
<span>override</span> <span><span>fun</span> <span>onCreate</span><span>(savedInstanceState: <span>Bundle</span>?)</span></span> {
<span>super</span>.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
<span>val</span> bottomNav = findViewById<BottomNavigationView>(R.id.bottom_navigation)
<span>// 仅在首次创建时初始化默认 Fragment</span>
<span>if</span> (savedInstanceState == <span>null</span>) {
<span>val</span> defaultFragment = getOrCreateFragment(R.id.nav_news, <span>"要闻"</span>)
supportFragmentManager.beginTransaction()
.add(R.id.fragment_container, defaultFragment)
.commit()
currentFragment = defaultFragment
}
bottomNav.setOnItemSelectedListener { item ->
<span>val</span> category = <span>when</span> (item.itemId) {
R.id.nav_news -> <span>"要闻"</span>
R.id.nav_tech -> <span>"科技"</span>
R.id.nav_entertainment -> <span>"娱乐"</span>
<span>else</span> -> <span>"要闻"</span>
}
switchFragment(getOrCreateFragment(item.itemId, category))
<span>true</span>
}
}
<span>private</span> <span>var</span> currentFragment: Fragment? = <span>null</span>
<span>private</span> <span><span>fun</span> <span>getOrCreateFragment</span><span>(itemId: <span>Int</span>, category: <span>String</span>)</span></span>: Fragment {
<span>return</span> fragmentMap.getOrPut(itemId) {
NewsFragment.newInstance(category)
}
}
<span>private</span> <span><span>fun</span> <span>switchFragment</span><span>(target: <span>Fragment</span>)</span></span> {
<span>if</span> (target == currentFragment) <span>return</span>
<span>val</span> transaction = supportFragmentManager.beginTransaction()
<span>// 隐藏当前 Fragment</span>
currentFragment?.let { transaction.hide(it) }
<span>// 如果目标未添加,则添加;否则直接显示</span>
<span>if</span> (!target.isAdded) {
transaction.add(R.id.fragment_container, target)
} <span>else</span> {
transaction.show(target)
}
transaction.commit()
currentFragment = target
}
}
6.5 项目优势说明
- 状态永久保留:使用 hide/show 而非 replace,切换导航不会重建视图,滚动位置、输入内容全保留。
- 杜绝重叠 BUG:通过 savedInstanceState == null 判断,避免系统重建时重复添加 Fragment。
- 性能更优:无需重复 inflate 布局,减少 IO 与渲染开销。
- 扩展性强:新增 Tab 只需添加菜单项和对应 Fragment,无需动核心架构。
七、总结与下篇预告
今天我们系统攻克了 Fragment 的核心玩法,要点梳理如下:
- ✅ Fragment 定位:Activity 内可复用的 UI 碎片,解决平板适配、底部导航和 UI 模块化。
- ✅ 加载方式:静态 XML 仅用于简单固定场景,动态事务加载是企业通用方案。
- ✅ 生命周期:依附宿主 Activity 联动,重点关注 onCreateView → onViewCreated 初始化视图,onDestroyView 清理引用。
- ✅ 通信规范:简单场景用接口回调,复杂场景优先共享 ViewModel,杜绝直接强转宿主。
- ✅ 避坑宝典:异步回调判空、savedInstanceState 防止旋转重叠。
- ✅ 实战案例:新闻分类底部导航,hide/show 切换保留页面状态,可复用骨架。
掌握了 Fragment,你就有了搭建复杂页面架构的利器。下一篇,我们将学习 Android 开发中最重要的列表组件 —— RecyclerView,解锁几乎所有 App 都离不开的高性能列表展示功能。
✨ 如果本文对你有帮助,欢迎点赞、收藏,让更多零基础的小伙伴少走弯路!
从生命周期、通信规范到 hide/show 状态保留与防重叠方案,实战要点讲得很透,适合正在做底部导航、多页面与平板适配的 Android 开发者。