第 6 篇|Fragment 碎片化艺术 —— 构建灵活可复用 UI 模块

第 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 都离不开的高性能列表展示功能。


✨ 如果本文对你有帮助,欢迎点赞、收藏,让更多零基础的小伙伴少走弯路!

相关推荐
JMchen1238 天前
第 3 篇|Android 项目结构解析与第一个界面 —— Hello, CSDN!
android·android studio·android 零基础·android 项目结构·android 界面开发
JMchen1239 天前
第 1 篇|Kotlin 基础入门 —— 变量、函数与空安全
开发语言·kotlin·android 入门·kotlin 空安全·android 零基础
灵感菇_3 个月前
Android Fragment全面解析
android·生命周期·fragment
哈哈~haha5 个月前
UI5_Walkthrough_Step 16: 对话框(Dialogs)和片段(Fragments)
dialog·ui5·fragment
个案命题1 年前
鸿蒙Ability对比Android的Fragment
android·华为·harmonyos·鸿蒙·fragment
缘来的精彩1 年前
Kotlin FragmentTransaction多容器管理多个fragment
android·kotlin·transaction·fragment
kidding7231 年前
前端VUE3的面试题
前端·typescript·compositionapi·fragment·teleport·suspense
命运之手2 年前
【Android】Fragment中监听Backpress返回键
android·fragment·backpress
飞鸟真人2 年前
使用supportFragmentManager管理多个fragment切换
android·tab·fragment·fragmentmanager