第 6 篇|Fragment 碎片化艺术 ------ 构建灵活可复用 UI 模块
掌握 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 让一套代码适配多种屏幕尺寸成为可能。
text
手机: [ Activity ] → 先显示 ListFragment,点击后替换为 DetailFragment
平板: [ Activity ] → 左侧 ListFragment + 右侧 DetailFragment 同时存在
④ 架构解耦,代码清爽
把一个复杂界面拆分成多个 Fragment,每个 Fragment 专注一小块逻辑,Activity 只当「舞台调度」,代码分层清晰,易于维护。
二、Fragment 的两种加载方式
2.1 静态加载:在 XML 中直接声明
在 Activity 布局文件中通过 标签引入,Fragment 随 Activity 一起初始化和销毁。
xml
<fragment
android:id="@+id/fragment_static"
android:name="com.example.HomeFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
适用场景 :页面结构完全固定、无需运行时切换的静态模块。
局限:不支持动态替换和移除,灵活性极差,实际项目中很少作为主力用法。
2.2 动态加载:FragmentTransaction 事务(企业主流)
通过代码获取 FragmentManager,开启事务,执行添加、替换、移除、隐藏等操作。这是企业开发的标准方式。
核心 API 一览:
| 方法 | 作用 |
|---|---|
| add(containerId, fragment) | 将 Fragment 添加到容器 |
| replace(containerId, fragment) | 替换容器中现有 Fragment |
| remove(fragment) | 移除 Fragment |
| hide(fragment) / show(fragment) | 隐藏/显示,视图不重建 |
| addToBackStack(name) | 加入返回栈,点返回键可撤销事务 |
标准动态添加模板:
kotlin
// 仅在 Activity 首次创建时添加,防止屏幕旋转重建导致重叠
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.add(R.id.fragment_container, HomeFragment())
.commit()
}
开发规范:企业开发统一使用动态加载,静态加载只用于极简单的固定模块展示。
三、Fragment 的完整生命周期
Fragment 生命周期比 Activity 更细化,而且完全依附于宿主 Activity。理解生命周期是解决 Fragment 空指针、内存泄漏、页面重叠的关键。
3.1 生命周期流程图
text
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 或访问控件
kotlin
// 在 Fragment 中直接强转宿主 Activity(极不推荐)
val activity = activity as MainActivity
activity.updateTitle("标题")
// 在 Activity 中直接操作 Fragment 内部控件
val fragment = supportFragmentManager.findFragmentById(R.id.container) as MyFragment
fragment.textView?.text = "Hello"
致命问题:
- Fragment 与宿主紧耦合,无法复用。
- 当 Fragment 处于后台、宿主被回收时,getActivity() 或 activity 返回 null,直接触发空指针崩溃。
4.2 ✅ 正确姿势一:接口回调(经典轻量方案)
适用于简单少量数据通信,通过接口解耦。
1. 定义通信接口
kotlin
interface FragmentCallback {
fun onDataReceived(data: String)
}
2. Activity 实现接口
kotlin
class MainActivity : AppCompatActivity(), FragmentCallback {
override fun onDataReceived(data: String) {
Log.d("Fragment通信", "收到:$data")
}
}
3. Fragment 通过接口回调传递数据
kotlin
class MyFragment : Fragment() {
private var callback: FragmentCallback? = null
override fun onAttach(context: Context) {
super.onAttach(context)
callback = context as? FragmentCallback
}
private fun sendMessage() {
callback?.onDataReceived("来自 Fragment 的数据")
}
override fun onDetach() {
super.onDetach()
callback = null // 释放引用
}
}
4.3 ✅ 正确姿势二:共享 ViewModel(官方推荐,企业主流)
适用于复杂页面、多个 Fragment 共享同一组数据。通过宿主 Activity 范围内的 ViewModel 进行通信,完全解耦,生命周期安全。
kotlin
// Activity 内共享的 ViewModel
class SharedViewModel : ViewModel() {
val selectedItem = MutableLiveData<String>()
}
// Fragment A:发送数据
class SenderFragment : Fragment() {
private val viewModel: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.findViewById<Button>(R.id.btn).setOnClickListener {
viewModel.selectedItem.value = "Hello from Fragment A"
}
}
}
// Fragment B:接收数据
class ReceiverFragment : Fragment() {
private val viewModel: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.selectedItem.observe(viewLifecycleOwner) { data ->
view.findViewById<TextView>(R.id.tv).text = data
}
}
}
优势: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 库(一般已有):
kotlin
implementation("com.google.android.material:material:1.11.0")
创建底部菜单 res/menu/bottom_nav_menu.xml:
xml
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/nav_news" android:title="要闻" />
<item android:id="@+id/nav_tech" android:title="科技" />
<item android:id="@+id/nav_entertainment" android:title="娱乐" />
</menu>
6.2 编写 NewsFragment
通过工厂方法传递分类参数,构建通用的新闻页面。
kotlin
class NewsFragment : Fragment() {
companion object {
private const val ARG_CATEGORY = "category"
fun newInstance(category: String) = NewsFragment().apply {
arguments = Bundle().apply { putString(ARG_CATEGORY, category) }
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.fragment_news, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val category = arguments?.getString(ARG_CATEGORY) ?: "要闻"
view.findViewById<TextView>(R.id.tv_title).text = "$category 新闻"
}
}
6.3 Activity 布局
xml
<androidx.constraintlayout.widget.ConstraintLayout ...>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/bottom_navigation" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:menu="@menu/bottom_nav_menu" />
</androidx.constraintlayout.widget.ConstraintLayout>
6.4 核心逻辑:hide/show 切换 + 防重叠
kotlin
class MainActivity : AppCompatActivity() {
private val fragmentMap = mutableMapOf<Int, Fragment>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val bottomNav = findViewById<BottomNavigationView>(R.id.bottom_navigation)
// 仅在首次创建时初始化默认 Fragment
if (savedInstanceState == null) {
val defaultFragment = getOrCreateFragment(R.id.nav_news, "要闻")
supportFragmentManager.beginTransaction()
.add(R.id.fragment_container, defaultFragment)
.commit()
currentFragment = defaultFragment
}
bottomNav.setOnItemSelectedListener { item ->
val category = when (item.itemId) {
R.id.nav_news -> "要闻"
R.id.nav_tech -> "科技"
R.id.nav_entertainment -> "娱乐"
else -> "要闻"
}
switchFragment(getOrCreateFragment(item.itemId, category))
true
}
}
private var currentFragment: Fragment? = null
private fun getOrCreateFragment(itemId: Int, category: String): Fragment {
return fragmentMap.getOrPut(itemId) {
NewsFragment.newInstance(category)
}
}
private fun switchFragment(target: Fragment) {
if (target == currentFragment) return
val transaction = supportFragmentManager.beginTransaction()
// 隐藏当前 Fragment
currentFragment?.let { transaction.hide(it) }
// 如果目标未添加,则添加;否则直接显示
if (!target.isAdded) {
transaction.add(R.id.fragment_container, target)
} else {
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 都离不开的高性能列表展示功能。
✨ 如果本文对你有帮助,欢迎点赞、收藏,让更多零基础的小伙伴少走弯路!