Fragment 入门教程:从核心概念到实践操作

前言

平板与手机之间最大的差别便是屏幕的大小,这种差别可能会导致同样的界面,在手机上看起来比较美观,但在平板上就有可能会出现控件过于大和长、元素之间空隙过大的情况。

所以我们要兼顾手机和平板的显示。为了解决这个问题并实现更灵活、模块化的界面设计,Android 引入了 Fragment。

Fragment 是什么

那么,Fragment 究竟是什么呢?简单来说,Fragment 是一种可以嵌入到 Activity 中、拥有自己布局和独立生命周期的可复用 UI 片段。

正是这种可嵌入和可复用的特性,Fragment 可以让程序可以更加充分、更加合理地利用大屏幕的空间(在平板上);通过切换不同的 Fragment 或在同一界面内使用多个 Fragment,来实现界面的导航和功能模块化(在手机上)。

实际上,你可以将 Fragment 理解为一个"迷你Activity"。和 Activity 类似,它也有自己的布局和独立的生命周期。

那怎么使用 Fragment 来充分利用平板屏幕的空间呢?

比如有一个新闻应用,其中有一个界面用来展示新闻列表,当点击任意一个新闻时,会打开另一个界面显示新闻的详细内容。如果是手机,我们可能会将新闻列表放在一个 Activity 中,将新闻的详细内容放在另一个 Activity 中,像下面这样:

平板也这样设计的话,新闻列表会过长,导致有大量空白区域。

所以我们会将新闻标题列表界面和新闻详细内容界面分别放在两个 Fragment 中,然后将这两个 Fragment 放到同一个 Activity 中,这样可以充分利用屏幕空间。

Fragment 的使用方式

我们现在来看看 Fragment 的用法,首先我们先在 Android Studio 中创建一个平板模拟器(Tablet)。

然后创建一个名为 FragmentTest 的 Empty Views Activity 项目。

简单用法

我们先来练练手:在一个 Activity 中添加两个 Fragment,并让这两个 Fragment 平分布局空间。

新建左侧 Fragment 的布局文件 left_fragment.xml,往布局中添加一个按钮,代码如下:

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="Button" />
    
</LinearLayout>

然后新建右侧 Fragment 的布局文件 right_fragment.xml,设置背景色,并且添加一个文本控件,代码如下:

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#f5f5f5"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="This is right fragment"
        android:textColor="#00BCD4"
        android:textSize="24sp" />
</LinearLayout>

新建一个 LeftFragment 类,继承自 Fragment。代码如下:

注意:选择 androidx.fragment.app 包下的 Fragment,而不是系统内置的 android.app.Fragment,因为系统内置的 Fragment 已经被废弃了。

kotlin 复制代码
class LeftFragment : Fragment() {

    // 使用 ViewBinding 视图绑定
    // _binding 变量只在 onCreateView 和 onDestroyView 方法之间是非空的
    private var _binding: LeftFragmentBinding? = null
    //binding 属性只在 _binding 变量不为空时有效,也就是 onCreateView 和 onDestroyView 方法之间
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?,
    ): View {
        _binding = LeftFragmentBinding.inflate(inflater, container, false)
        return binding.root
    }


    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null // 在视图销毁时解除绑定,防止内存泄漏
    }
}

注意:这里我们使用了视图绑定来访问视图,_bing 变量持有绑定对象的引用,而 binding 属性只是对 _binding 变量的非空访问。还不熟悉的可以看官方文档

在上述代码中,我们重写了 Fragment 的 onCreateView() 方法,在方法内部,我们加载了布局并创建了绑定对象,最后将根视图的引用进行返回。在 onDestroyView() 中,我们清理了绑定对象的引用。

同样地,我们创建 RightFragment,代码如下所示:

kotlin 复制代码
class RightFragment : Fragment() {

    private var _binding: RightFragmentBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?,
    ): View {
        _binding = RightFragmentBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

然后修改 activity_main.xml 布局文件,修改后的代码如下:

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/leftFrag"
        android:name="com.example.fragmenttest.LeftFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/rightFrag"
        android:name="com.example.fragmenttest.RightFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />
</LinearLayout>

我们通过了 <androidx.fragment.app.FragmentContainerView> 标签来往布局中添加 Fragment,使用时,需要通过 android:name 属性来指定当前要添加的 Fragment 类,填入全类名。FragmentContainerView 是承载 Fragment 的推荐容器,替代了之前使用的 <fragment>。

运行效果:

简单的使用知道了后,我们来看 Fragment 更加高级的用法。

动态添加Fragment

Fragment 可以在程序运行时动态地添加到 Activity 中。

我们新建一个 another_right_fragment.xml 布局文件,代码如下所示:

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#FFEB3B"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="This is another right fragment"
        android:textColor="#00BCD4"
        android:textSize="24sp" />
    
</LinearLayout>

然后新建 AnotherRightFragment 作为另一个右侧的 Fragment,代码如下:

kotlin 复制代码
class AnotherRightFragment : Fragment() {

    private var _binding: AnotherRightFragmentBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?,
    ): View {
        _binding = AnotherRightFragmentBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

修改 activity_main.xml 布局文件,修改后:

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/leftFrag"
        android:name="com.example.fragmenttest.LeftFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />

    <FrameLayout
        android:id="@+id/rightFrag"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />
</LinearLayout>

我们将右侧的装载 Fragment 的容器由 FragmentContainerView (静态加载)换成了 FrameLayout 帧布局。

然后在 LeftFragment 中添加一个方法为按钮注册点击事件。

kotlin 复制代码
class LeftFragment : Fragment() {

    private var _binding: LeftFragmentBinding? = null
    private val binding get() = _binding!!

    private var onClick: () -> Unit = { }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?,
    ): View {
        _binding = LeftFragmentBinding.inflate(inflater, container, false)

        binding.button.setOnClickListener {
            onClick()
        }

        return binding.root
    }

    /**
     * 设置按钮的点击监听器
     */
    fun setButtonClickListener(onClick: () -> Unit) {
        this.onClick = onClick
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

最后来到 MainActivity 中,使用代码往 FrameLayout 布局中添加内容,从而实现动态添加 Fragment 的功能。代码如下:

kotlin 复制代码
class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // 获取 LeftFragment 的实例
        val leftFragment = supportFragmentManager.findFragmentById(R.id.leftFrag) as? LeftFragment

        leftFragment?.let { fragment ->
            fragment.setButtonClickListener {
                replaceFragment(AnotherRightFragment())
            }
        }

        replaceFragment(RightFragment()) // 初始加载 RightFragment
    }

    private fun replaceFragment(fragment: Fragment) {
        val fragmentManager = supportFragmentManager
        val transaction = fragmentManager.beginTransaction()

        // 你也可以通过视图绑定获取右侧 FrameLayout 视图,从而获取其id
        // val containerView: FrameLayout = binding.rightFrag // 获取View对象
        // transaction.replace(containerView.id, fragment) // 使用其id
        
        // 直接使用ID
        transaction.replace(R.id.rightFrag, fragment) 

        transaction.commit()
    }
}

我们利用 LeftFragment 中提供的 setButtonClickListener 自定义方法给左侧 Fragment 中的按钮注册了点击事件,然后调用了 replaceFragment 方法动态添加了 RightFragment。在按钮的点击回调中,我们也调用了 replaceFragment() 方法会将右侧的 Fragment 替换成 AnotherRightFragment。

在 replaceFragment() 方法中,我们先会获取 FragmentManager 的实例,然后通过 FragmentManager 的 beginTransaction() 方法开始一个事务,通过 FragmentTransaction 的 replace() 方法来添加或替换容器中的 Fragment,传入容器id和Fragment实例,最后调用 FragmentTransaction 的 commit() 方法来提交事务。

这样就完成了在 Activity 中动态添加 Fragment 的功能,运行效果:

在 Fragment 中实现返回栈

现在我们按下返回键会直接退出程序,如果我们想要实现类似返回栈的效果,按下返回键可以回到上一个 Fragment,该如何实现?

其实很简单,在提交事务之前,调用一下 FragmentTransaction 的 addToBackStack() 方法即可,它会将当前事务添加到返回栈中。

代码如下:

kotlin 复制代码
private fun replaceFragment(fragment: Fragment) {
    val fragmentManager = supportFragmentManager
    val transaction = fragmentManager.beginTransaction()

    transaction.replace(R.id.rightFrag, fragment) // 使用容器id
    transaction.addToBackStack(null) // 将此事务添加到返回栈
    transaction.commit()
}

其中 addToBackStack 方法接收一个 name 参数,用来描述返回栈的状态,我们直接传入 null 即可。

现在的运行效果:点击按钮会将 AnotherRightFragment 添加到 Activity 中,按下返回键会移除 AnotherRightFragment ,回到之前的 RightFragment 界面,再次按下返回键,会移除 RightFragment 界面,右侧的 FrameLayout 容器会为空,如图所示:

再次按下返回键,程序才会退出。

Fragment和Activity之间的交互

虽然 Fragment 是嵌入在 Activity 中进行显示的,但它们之间是相对独立的组件,并没有什么方式来直接进行交互。但还是可以实现它们之间的通信和交互的。

如果你想要在 Activity 中调用 Fragment 中的方法:

其实我们之前的示例中已经展示了第一种方法,那就是通过 FragmentManager 的 findFragmentById 方法从布局文件中获取 Fragment 的实例,例如:

kotlin 复制代码
// 获取 LeftFragment 的实例 
val leftFragment = supportFragmentManager.findFragmentById(R.id.leftFrag) as? LeftFragment

然后我们就可以随意调用其内部的公有方法了,例如:

kotlin 复制代码
leftFragment?.let { fragment ->
    fragment.setButtonClickListener {
        replaceFragment(AnotherRightFragment())
    }
}

另外,由于当前 Fragment 的容器是 FragmentContainerView,所以我们也可以通过 FragmentContainerView 的 getFragment() 方法来获取 Fragment 实例。像这样:

kotlin 复制代码
// 获取 LeftFragment 的实例
val leftFragment = binding.leftFrag.getFragment<LeftFragment>()
leftFragment.let { fragment ->
    fragment.setButtonClickListener {
        replaceFragment(AnotherRightFragment())
    }
}

第二种是现代且直接的方式。但你要注意调用时机,确保 Fragment 已经成功添加到 FragmentManager 中了。

那在 Fragment 中又该如何获取其对应 Activity 实例呢?

超简单,可以直接调用 getActivity() 方法即可获取当前 Fragment 所对应的 Activity 实例,但它可能会返回空。更安全的方式是使用 requireActivity 方法。它会在 Fragment 未附加到 Activity 上时,抛出 IllegalStateException 异常,从而能够获取到一个非空的 Activity 实例,其实其内部只是进行了 if 判空,源码:

java 复制代码
@NonNull
public final FragmentActivity requireActivity() {
    FragmentActivity activity = getActivity();
    if (activity == null) {
        throw new IllegalStateException("Fragment " + this + " not attached to an activity.");
    }
    return activity;
}

另外,在 Fragment 的 onAttach() 回调方法中,context 参数其实就是相关联的 Activity 的实例。

最后,不同的 Fragment 之间可以通信吗?

答案是肯定的。首先一个 Fragment 可以和与其相关联的 Activity 通信,而该 Activity 又可和另一个 Fragment 通信,所以能行。另外,你也可以通过使用共享的 ViewModel 来完成通信。

相关推荐
五彩小白12 小时前
自回归和上下文
android
事圆则缓14 小时前
Android 多渠道打包实战:Product Flavors、签名、AAB 与发包校验
android
树下困觉眯16 小时前
View绘制流程学习--获取WindowManager的三种方式
android·系统架构·view design
vilya17 小时前
我怎么给手机 GUI Agent 做双通道感知:无障碍树为主,投屏像素兜底
android·人工智能
李子红了时18 小时前
阿狸舞台APP(安卓端手机EncFS解密+压缩包解压+视频播放)
android·音视频
事圆则缓18 小时前
Android 使用 Jenkins 实现 CI/CD,并用 SonarQube 建立质量门禁
android·ci/cd·jenkins
其实防守也摸鱼19 小时前
DeepSeek Harness 开源贡献手记:从 Issue 到 Merge 的完整旅程
android·数据库·学习·ai·oracle·自动化
恋猫de小郭19 小时前
Android CLI 支持 AI Agent 通过 Device Streaming 调试云真机
android·前端·flutter
素师良码2 天前
第5篇:显示驱动必备调试工具浅析
android
维克兜率天2 天前
【维克】配对交易的季节性:哪些品种适合长拿?
android·开发语言·笔记·python·算法·kotlin·量化