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

文章来源声明: 原文作者:Android_实战之道; 来源站点:掘金; 原文链接:https://juejin.cn/post/7684659924174815258; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

从生命周期、通信规范到 hide/show 状态保留与防重叠方案,实战要点讲得很透,适合正在做底部导航、多页面与平板适配的 Android 开发者。

第 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 让一套代码适配多种屏幕尺寸成为可能。

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


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