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 的实例,然后通过 FragmentManagerbeginTransaction() 方法开始一个事务,通过 FragmentTransactionreplace() 方法来添加或替换容器中的 Fragment,传入容器id和Fragment实例,最后调用 FragmentTransactioncommit() 方法来提交事务。

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

在 Fragment 中实现返回栈

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

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

代码如下:

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 中的方法:

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

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

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

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

另外,由于当前 Fragment 的容器是 FragmentContainerView,所以我们也可以通过 FragmentContainerViewgetFragment() 方法来获取 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 来完成通信。

相关推荐
CYRUS_STUDIO3 小时前
FART 脱壳某大厂 App + CodeItem 修复 dex + 反编译还原源码
android·安全·逆向
Shujie_L6 小时前
【Android基础回顾】四:ServiceManager
android
Think Spatial 空间思维6 小时前
【实施指南】Android客户端HTTPS双向认证实施指南
android·网络协议·https·ssl
louisgeek7 小时前
Git 使用 SSH 连接
android
二流小码农7 小时前
鸿蒙开发:实现一个标题栏吸顶
android·ios·harmonyos
八月林城8 小时前
echarts在uniapp中使用安卓真机运行时无法显示的问题
android·uni-app·echarts
雨白8 小时前
搞懂 Fragment 的生命周期
android
casual_clover9 小时前
Android 之 kotlin语言学习笔记三(Kotlin-Java 互操作)
android·java·kotlin
梓仁沐白9 小时前
【Kotlin】数字&字符串&数组&集合
android·开发语言·kotlin
技术小甜甜9 小时前
【Godot】如何导出 Release 版本的安卓项目
android·游戏引擎·godot