Fragment 的使用
Android 学习笔记 —— Fragment 的使用
Fragment
Fragment 表示应用界面中可重复使用的一部分。Fragment 定义和管理自己的布局,具有自己的生命周期,并且可以处理自己的输入事件。Fragment 不能独立存在,而是必须由 Activity 或另一个 Fragment 托管。Fragment 的视图层次结构会成为宿主的视图层次结构的一部分,或附加到宿主的视图层次结构。
上面是官方对 Fragment 的介绍。其实 Fragment 和 Activity 很相似,但使用 Fragment 可以将一个完整的界面划分为离散的区块,然后嵌入到 Activity 中,从而将模块化和可重用性引入 Activity 的界面。说白了就是可以在不切换 Acitivty 的同时变更 UI 界面中某一区域的内容,Activity 适合放置全局元素,而 Fragment 更适合定义和管理单个屏幕或部分屏幕的界面。
Fragment 的简单使用
使用步骤:
-
编写 Fragment 的布局文件 fragment_left.xml 和 fragment_right.xml。
<!-- fragment_left.xml --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:id="@+id/button_left_fragment" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:text="Button" /> </LinearLayout><!-- fragment_right.xml --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:text="This is right fragment." /> </LinearLayout> -
分别创建对应的 Fragment 类 LeftFragment 和 RightFragment,并继承自 Fragment。重写
onCreateView()方法。public class LeftFragment extends Fragment { @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { // 使用布局加载器去加载对应 Fragment 的布局文件,转化成 View 对象并返回 return inflater.inflate(R.layout.fragment_left, container, false); } } -
在 Activity 的布局文件中添加
<fragment>标签,指定android:name属性为对应的 Fragment 类。<fragment android:id="@+id/left_fragment" android:name="com.amie.test.LeftFragment" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" /> <fragment android:id="@+id/right_fragment" android:name="com.amie.test.RightFragment" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" />
动态添加 Fragment
接着上面的例子继续添加一个 Fragment。
-
编写一个新的 Fragment 布局文件 fragment_another_right.xml。
<!-- fragment_another_right.xml --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#ffff00" 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." /> </LinearLayout> -
创建对应 Fragment 类 AnotherRightFragment,并继承自 Fragment。重写
onCreateView()方法。public class AnotherRightFragment extends Fragment { @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { // 使用布局加载器去加载对应 Fragment 的布局文件,转化成 View 对象并返回 return inflater.inflate(R.layout.fragment_another_right, container, false); } } -
修改 Activity 的布局文件,将右边 Fragment 的替换成
<FrameLayout>。因为要把动态加载的 Fragment 放入一个布局中,这个布局就只有这个 Fragment 没有其他东西,也不需要任何定位,那么会默认把控件放到左上角的 FrameLayout 就很适合。这里先不采用官方建议的
FragmentContainerView替代<fragment>标签和<FrameLayout>标签。<fragment android:id="@+id/left_fragment" android:name="com.amie.test.LeftFragment" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" /> <FrameLayout android:id="@+id/right_frame_layout" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" /> -
在 Activtiy 中给 FrameLayout 添加内容,实现动态添加 Fragment。
public class FragmentActivity extends AppCompatActivity implements View.OnClickListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_fragment); Button button = (Button) findViewById(R.id.button_left_button); button.setOnClickListener(this); replaceFragment(new RightFragment()); } /** * 创建一个用来替换 Fragment 的方法 * @param fragment 需要被替换成的 Fragment 的实例 */ private void replaceFragment(Fragment fragment) { // 调用 getSupportFragmentManager() 方法获取 FragmentManager 对象 FragmentManager fragmentManager = getSupportFragmentManager(); // 通过 FragmentManager 调用 beginTransaction() 开启一个事务并获得 FragmentTransaction 对象 FragmentTransaction transaction = fragmentManager.beginTransaction(); // 向容器内添加或替换 Fragment,可通过 replace() 方法实现 // 第一个参数是容器的资源 ID,第二个参数是需要被替换成的 Fragment 实例对象 transaction.replace(R.id.right_frame_layout, fragment); // 提交事务 transaction.commit(); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.button_left_button: replaceFragment(new AnotherRightFragment()); break; } } }
在 Fragment 中模拟返回栈,返回上一个 Fragment
private void replaceFragment(Fragment fragment) {
// 调用 getSupportFragmentManager() 方法获取 FragmentManager 对象
FragmentManager fragmentManager = getSupportFragmentManager();
// 通过 findFragmentById() 方法获取当前 Fragment
Fragment current = fragmentManager.findFragmentById(R.id.right_frame_layout);
if (current != null) {
// 当前 Fragment 不为空时,判断是否与目标 Fragment 类型相同,减少重复创建
if (current.getClass() == fragment.getClass()) {
return;
}
}
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.right_frame_layout, fragment);
// 将此事务添加到后台堆栈。这意味着事务将在提交后被记住,并在以后从堆栈中弹出时将其操作反转
// 传入 null 即可,其他字符串也行
transaction.addToBackStack(null);
// 提交事务
transaction.commit();
}
Fragment 和 Activity 之间进行通信
前面已经知道在 Activity 中通过 getSupportFragmentManager().findFragmentById() 方法获取对应的 Fragment 对象,那也就可以调用 Fragment 的方法进行操作了。但是在 Fragment 中如何调用 Activity 中的方法呢?其实很简单,在 Fragment 中直接调用 getActivity() 就可以获取到 Activity 对象了(也可以使用 requireActivity(),内部多了一个判空操作)。而 Fragment 与 Fragment 之间通信则可以通过 Activity 作为跳板来实现。
使用限定符动态加载布局
前面使用的 Fragment 是显示在平板等大屏设备的双页模式下,但如果要在手机上运行应该显示成单页模式,那如何判断应该显示单页还是双页模式呢?这里可以在 res 资源目录下新建 layout-large 文件夹,在该文件夹下建立一个同名的布局文件,在这里编写双页模式的布局。这里的 large 就是一个限定符(Qualifier),系统会帮我们判断当前运行的设备是否是 large 大屏设备,然后加载对应的布局。当然,代码中的逻辑处理也需要改动一下。
large 限定符可以解决大屏设备上的单双页判断问题,但是我们并不知道 large 限定的大屏是多大。对此,我们还可以使用最小限定符(Smallest-width Qualifier),它可以指定屏幕宽度的最小值(以 dp 为单位),但屏幕宽度大于该值就加载这里的布局。如,在 res 资源目录下新建 layout-sw600dp 文件夹。
Fragment 的生命周期
API 28 之前的旧版 Fragment 生命周期:

- onAttach(): 当 Fragment 和 Activity 相关联时调用。可以通过该方法获取 Activity 引用,还可以通过
getArguments()获取参数。 - onCreate(): 当 Fragment 被创建时调用。
- onCreateView(): 当 Fragment 的布局被创建时调用。
- onActivityCreated(): 当 Activity 完成
onCreate()时调用,已被弃用。 - onStart(): 当 Fragment 可见时调用。
- onResume(): 当 Fragment 可见且可交互时调用。
- onPause(): 当 Fragment 不可交互但可见时调用。
- onStop(): 当 Fragment 不可见时调用。
- onDestroyView(): 当 Fragment 的从视图结构中移除时调用。
- onDestroy(): 当销毁 Fragment 时调用。
- onDetach(): 当 Fragment 和 Activity 解除关联时调用。
API 28 之后 的新版 Fragment 生命周期:

新版 API 主要表现在 onActivityCreated() 被弃用,onViewCreated() 出现。将在后续篇章 onActivityCreated() 被弃用 中再作说明。
Fragment 生命周期状态和回调
在确定 Fragment 的生命周期状态时,FragmentManager 请考虑以下几点:
- Fragment 的最大状态由它的 FragmentManager 决定。 一个 Fragment 不能超越 FragmentManager 的状态。
- 作为 FragmentTransaction 的一部分,您可以使用
setMaxLifecycle()为 Fragment 设置最大生命周期状态。 - Fragment 的生命周期状态永远不能大于其父级。例如,父 Fragment 或 Activity 必须在其子 Fragment 之前启动。同样,子 Fragment 必须在其父 Fragment 或 Activity 之前停止。
注意:避免使用
<fragment>标签来添加使用 XML 的Fragment ,因为<fragment>标签允许 Fragment 状态超出其 FragmentManager 状态. 相反,应该使用 FragmentContainerView 用于 XML 添加 Fragment 。
androidx.fragment.app.FragmentContainerView
FragmentContainerView 是专为 Fragments 设计的自定义 Layout。它扩展了 FrameLayout,因此它可以可靠地处理 Fragment Transactions,并且它还具有与 Fragment 行为协调的附加功能。
官方已经建议将 <fragment> 标签和用于切换 Fragment 的 <FrameLayout> 标签替换成 androidx.fragment.app.FragmentContainerView 了。那在代码中又要做出什么更改呢?
在上面的例子中,如果直接替换容器的 <FrameLayout> 标签而不修改其他代码是没有问题的,不会影响程序运行。但是如果直接替换 <fragment> 标签就会出现在 Activity 中无法直接通过 findViewById() 获取到 Fragment 中的控件,这就会导致控件的 setOnClickListenner() 失败,报空指针异常。
因为使用 <FrameLayout> 标签是需要动态加载 Fragment 的,而 androidx.fragment.app.FragmentContainerView 继承自 FrameLayout,也需要动态加载,所以替换后并不需要修改逻辑代码。动态加载的 Fragment,想要获取其内部控件必须要在 onCreateView() 中得到 Fragment 的布局 View 之后才可以。
观察 Fragment 的源码发现,其中有个 onViewCreated(View view, Bundle savedInstanceState) 方法。该方法在 onCreateView() 结束后立即调用,其中的参数 View 正是 onCreateView() 返回的视图。
onCreateView()一般用于初始化 Fragment 的视图,onViewCreated()一般用于初始化视图内各个控件,而onCreate()用于初始化与 Fragment 视图无关的变量。
到这里其实就很好解决了,在 Fragment 中获取控件,然后将事件监听对象设置为 Activity 就好了。下面是修改后的代码。
public class LeftFragment extends Fragment {
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.left_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// 获取对应 Activity
FragmentActivity activity = (FragmentActivity) getActivity();
// 获取控件实例
Button button1 = view.findViewById(R.id.button1_left_fragment);
// 将控件的事件监听交给 Activity 处理
// 让 Activity 处理是因为 FragmentManager 在 Activity 中
button.setOnClickListener((View.OnClickListener) activity);
}
}
<!-- activity_fragment.xml -->
<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/left_fragment_container"
android:name="com.amie.firstlineofcode.LeftFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/right_fragment_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>
// 依旧实现 View.OnClickListener 接口
public class FragmentActivity extends AppCompatActivity implements View.OnClickListener {
// ...
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.button_left_fragment:
replaceFragment(new AnotherRightFragment());
break;
}
}
}
onActivityCreated() 被弃用
onActivityCreated() 是在宿主 Activity 的 onCreate() 完成之后立即调用,这也确保了宿主 Activity 的视图是完成了初始化。当然在这个方法内也可以操作宿主 Activity 视图的控件 View 或者获知其他 Fragment。
执行该方法时,与 Fragment 绑定的 Activity 的 onCreate() 方法已经执行完成并返回,在 onActivityCreated() 内可以进行与 Activity 交互的 UI 操作。但是,如果在 onActivityCreated() 之前 Activity 的 onCreate() 方法尚未执行完成,提前进行交互操作,会引发空指针异常。
替代方案:
谷歌为了管理 Fragment 的生命周期,实现了 LifecycleOwner,暴露了一个可以通过getLifecycle() 方法访问的 Lifecycle 对象。
因为 onActivityCreated() 是宿主 Activity 的 onCreate() 之后立即调用,所以可以在 onAttach() 的时候,通过订阅 Activity 的 lifecycle 来获取 Activity 的 onCreate() 事件,记得要 removeObserver()。
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
//requireActivity() 返回的是宿主 Activity
requireActivity().getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
if (event.getTargetState() == Lifecycle.State.CREATED){
// TODO
getLifecycle().removeObserver(this); // 最后删除观察者
}
}
});
}