重温 Fragment 分析二:使用及注意事项

文章目录
  1. 1. 嵌入方式
  2. 2. 回退栈
  3. 3. 通信方式
  4. 4. 参考文献

Everything is OK in the end. If it’s not OK, then it’s not the end.

上一篇重温 Fragment 分析一:生命周期中简单分析总结了 Fragment 的生命周期,这一期着重分析并归纳 Fragment 常见的使用及注意事项。

我们知道,作为 Activity 界面的一部分,Fragment 的存在必须依附于 Activity,且与 Activity 一样,有着自己的生命周期,同时处理用户的交互动作。此外,同一个 Activity 可以有一个或多个 Fragment 作为界面内容,并且可以动态添加、删除 Fragment,灵活地控制 UI 内容,也可以用来解决部分屏幕适配的问题。

需要注意的是,support v4 包中也提供了 Fragment,兼容 Android 3.0 之前的系统,使用兼容包需要留意两点:

  • Activity 须继承自 FragmentActivity;
  • 须使用 getSupportFragmentManager() 方法获取 FragmentManager 对象。

嵌入方式

Fragment 向宿主 Activity 贡献一部分 UI,作为 Activity 总体视图层次结构的一部分嵌入 Activity 中,分为两种方式:布局静态嵌入和代码动态嵌入。

布局静态嵌入

在 Activity 的 Layout 布局中使用<fragment>嵌入指定的 Fragment,如:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">


<fragment
android:layout_width="match_parent"
android:layout_height="match_parent"/>


</FrameLayout>

当系统创建此 Activity 布局时,会实例化在布局中指定的每个 Fragment,并为每个 Fragment 调用 onCreateView() 方法,来检索每个 Fragment 的布局。系统会直接嵌入 Fragment 返回的 View 来替代<fragment>元素。

注意

每个 Fragment 都需要一个唯一的标识符,重启 Activity 时,系统可以使用该标识符来恢复 Fragment (也可以使用标识符来捕获 Fragment 以执行某些事务,如将其移除)。一般通过三种方式为 Fragment 提供 ID:

  • 为 android:id 属性提供唯一的 ID;
  • 为 android:tag 属性提供唯一的字符串;
  • 若未给以上两个属性提供值,系统会使用容器视图的 ID。

代码动态嵌入

在 Activity 运行期间将 Fragment 添加到 Activity 布局中,需要指定将 Fragment 放入哪个 ViewGroup 中。

要想在 Activity 中执行 Fragment 事务(如添加、移除或替换 Fragment),可以像下面这样从 Activity 中获取一个 FragmentTransaction 实例:

1
2
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

然后,可以使用 add() 方法添加一个 Fragment,指定要添加的 Fragment 以及将其插入哪个视图。如:

1
2
3
DemoFragment fragment = new DemoFragment();
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();

传递到 add() 的第一个参数是 ViewGroup,即放置 Fragment 的位置,由资源 ID 指定;第二个参数是要添加的 Fragment。一旦通过 FragmentTransaction 作出了更改,须调用 commit() 使更改生效。

小结如下:

I. 关于 Fragment 常用的三个类:

android.app.Fragment:主要用于定义 Fragment;

android.app.FragmentManager:主要用于在 Activity 中操作 Fragment;

android.app.FragmentTransaction:保证一系列 Fragment 操作的原子性。

II. 获取 FragmentManager 的方式:

getFragmentManager();v4 中,getSupportFragmentManager()。

III. FragmentTransaction 的方法处理主要的操作:

FragmentTransaction transaction = fragmentManager.beginTransaction();开启一个事务。

transaction.add():向 Activity 中添加一个 Fragment;

transaction.remove():从 Activity 中移除一个 Fragment,若被移除的 Fragment 未被添加到回退栈,则 Fragment 实例会被销毁;

transaction.replace():用另一个 Fragment 替换当前的,即 remove() 和 add() 的共通操作;

transaction.hide():隐藏当前的 Fragment,仅设为不可见,并不销毁;

transaction.show():显示之前隐藏的 Fragment;

detach():将 View 从 UI 中移除,同 remove() 不一样,这时候,fragment 的状态仍然由 FragmentManager 维护;

attach():重建 View 视图,依附至 UI 并显示:

transaction.commit():提交一个事务,注意,commit() 方法要在 Activity.onSaveInstance() 之前调用。

开发中注意的几点:

a. 若在 Fragment A 中的 EditText 中填充了一些数据,且 EditText 中未做特殊处理,先切换到 Fragment B,希望回到 Fragment A 时填充的数据还在,则适合调用的就是 hide() 和 show() 方法;

b. 若不希望保留用户操作,可以先使用 remove(),然后 add();或者,使用 replace(),和先 remove() 后 add() 的效果一致;

c. remove() 和 detach() 的区别:不考虑回退栈的情况下,remove() 会销毁整个 Fragment 实例,而 detach() 只销毁其视图结构,不会销毁实例。

回退栈

我们知道,Android 系统为 Activity 维护了一个任务栈,同理,我们也可以通过 Activity 维护一个回退栈,以保存每次 Fragment 事务发生的变化。若将 Fragment 任务添加到回退栈,当用户点击返回按钮时,将看到上一次保存的 Fragment。紧接着,若 Fragment 完全从回退栈中弹出,用户再次点击返回按钮,则最终会退出 Activity。

为达到这样一个效果:

点击按钮一,跳至界面二,再点击按钮二,跳至界面三,最后,点击 Back 键顺序返回。用户点击 Back 键,即是 Fragment 回退栈不断地弹栈。详见FragmentBackStackDemo,具体代码如下:

Activity 的布局文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">


<FrameLayout
android:id="@+id/fl_content"
android:layout_width="match_parent"
android:layout_height="match_parent">

</FrameLayout>

</RelativeLayout>

Fragment 就显示在 FrameLayout 中,MainActivity 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);

FragmentOne fragmentOne = new FragmentOne();
FragmentManager manager = getFragmentManager();
FragmentTransaction transaction = manager.beginTransaction();
transaction.add(R.id.fl_content, fragmentOne, "One");
transaction.commit();
}

}

事实上,另一种更严谨的写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MainActivity extends AppCompatActivity {

private FragmentOne mFragmentOne;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);

FragmentManager manager = getFragmentManager();
mFragmentOne = (FragmentOne) manager.findFragmentById(R.id.fl_content);

if (mFragmentOne == null) {
mFragmentOne = new FragmentOne();
manager.beginTransaction().add(R.id.fl_content, mFragmentOne).commit();
}
}

}

就是将 FragmentOne 添加到布局文件的 FrameLayout 中,这里没有调用 addToBackStack(String) 方法,为了避免在当前显示时,点击 Back 键出现白屏。FragmentOne 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class FragmentOne extends Fragment implements View.OnClickListener {

private Button mButton;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_one, container, false);
mButton = (Button) view.findViewById(R.id.btn_one);
mButton.setOnClickListener(this);
return view;
}

@Override
public void onClick(View v) {
FragmentTwo fragmentTwo = new FragmentTwo();
FragmentManager manager = getFragmentManager();
FragmentTransaction transaction = manager.beginTransaction();
transaction.replace(R.id.fl_content, fragmentTwo, "TWO");
transaction.addToBackStack(null);
transaction.commit();
}

}

点击 FragmentOne 中的按钮,调用 replace() 方法。事实上,若不添加任务到回退栈,前一个 Fragment 实例会被销毁;这里,调用 addToBackStack(null) 方法,将当前的事务添加到回退栈,FragmentOne 的实例不会被销毁,但是视图层次会被销毁,即调用到了 onDestroyView() 和 onCreateView(),可以看到动图中,一开始在编辑框中输入的内容,再次返回到 FragmentOne 界面时消失了。

FragmentTwo 的代码就不贴出来了,和 FragmentOne 的区别在,点击时,没有使用 replace(),即先隐藏了当前的 Fragment,接着,添加了 FragmentThree 的实例,最后,将事务添加到回退栈。目的在于,不希望视图重绘,动图中,可以看到,在 FragmentTwo 的编辑框中输入的内容,用户点返回时,数据一直都在。

同样,FragmentThree 只是弹出一个 Toast,借此了解回退栈和 hide(),replace() 的应用场景。

通信方式

Fragment 与 Activity 的通信有三种情形:

  • Activity 操作内嵌的 Fragment;
  • Fragment 操作宿主 Activity;
  • Fragment 操作同属 Activity 中的其他 Fragment。

I. Activity 持有所有内嵌的 Fragment 对象实例(创建实例时保存的 Fragment 对象,或者通过 FragmentManager 类提供的 findFragmentById() 和 findFragmentByTag() 方法获取到的 Fragment 对象),故可以直接操作 Fragment;

II. Fragment 通过 getActivity() 方法获取到宿主 Activity 对象(强制转换类型),进而操作宿主 Activity;

III. 获取到宿主 Activity 对象的 Fragment 可以方便地操作其他 Fragment 对象。

注意,在 Fragment 中需要 Context,则调用 getActivity();若 Context 需要在 Activity 销毁后还存在,则使用 getActivity().getApplicationContext()。

下面关注下 Fragment 与 Activity 通信的最佳实践

上面的操作能够解决 Activity 与 Fragment 的通信,但是代码显得有点紊乱,也不具有高内聚与低耦合的特点,故推荐:Fragment 做自己该做的,其之间的控制显示操作,就交由宿主 Activity 统一管理。采用对外开放接口的形式将 Fragment 的一些对外面的操作传递给宿主 Activity。详见 FragmentAndActivityChatDemo,FragmentOne 重构后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class FragmentOne extends Fragment implements View.OnClickListener {

private Button mButton;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_one, container, false);
mButton = (Button) view.findViewById(R.id.btn_one);
mButton.setOnClickListener(this);
return view;
}

/**
* 转交给宿主 Activity 处理
*
* @param v
*/

@Override
public void onClick(View v) {
if (getActivity() instanceof ButtonOneClickListener) {
((ButtonOneClickListener) getActivity()).onButtonOneClick();
}
}

/**
* 按钮点击回调
*/

public interface ButtonOneClickListener {
void onButtonOneClick();
}

}

FragmentOne 不和任何 Activity 耦合,任何 Activity 都可以使用;同时,声明了一个接口,回调其点击事件,管理其点击事件的 Activity 可以实现此接口。在 onClick() 中有判断当前绑定的 Activity 是否实现接口,实现了则直接调用。

FragmentTwo 中,与 FragmentOne 的代码大体相似,新提供了设置监听器的方法,即 Activity 要实现接口,且要显示调用 mFragmentTwo.setButtonTwoClickListener(this)。

综上,Fragment 和 Activity 虽然通过 getActivity()、findFragmentById() 和 findFragmentByTag() 的方法执行任何相关的操作,也能在一个 Fragment 里操作另一个 Fragment,但是不提倡。Activity 在 Fragment 之间担任着主管一样的角色,其来决定操作 Fragment 的方式。即使 Fragment 无法响应 Intent,但是 Activity 能接收 Intent,由参数判断显示要显示的 Fragment。

getActivity() 的引用

上面提到的,以及在日常开发中,在 Fragment 中通过 getActivity() 获取到宿主 Activity 对象,然而,没注意的话,会导致下面两个常见的问题:

  • Activity 实例的销毁。如 Fragment 中存在像网络请求一类的异步耗时任务,当任务执行完毕,回调 Fragment 的方法并回到宿主 Activity 对象时,有可能宿主 Activity 对象已销毁,从而引发 NPE 异常,甚至导致程序崩溃。因此,异步回调时要有注意添加空值的判断(如 getActivity() != null) 等,或者在 Fragment 创建实例时通过 getActivity().getApplicationContext() 方法来保存整个应用的上下文对象;
  • 内存泄漏问题。若 Fragment 持有宿主 Activity 的引用,会导致宿主 Activity 无法回收,造成内存泄漏。故最好不要在 Fragment 中持有宿主 Activity 的引用。

关于 Context 上下文的引用,Fragment 提供了一个 onAttach() 方法,此方法中获取 Context 对象,如:

1
2
3
4
5
@Override
public void onAttach(Context context) {
super.onAttach(context);
this.context = context;
}

Fragment 中也提供了一个 getContext() 方法,来返回 Context 对象,若不使用具体的宿主 Activity 对象,可用这个方法获取 Context 对象。

至此,关于 Fragment 常见的使用和注意事项分析完毕,重温 Fragment 系列两篇文章到此结束。

本人才疏学浅,如有疏漏错误之处,望读者中有识之士不吝赐教,谢谢。

1
Email: [email protected] / WeChat: Wolverine623

您也可以关注我个人的微信公众号码农六哥第一时间获得博客的更新通知,或后台留言与我交流

参考文献

1.https://developer.android.com/guide/components/fragments.html?hl=zh-cn

2.http://blog.csdn.net/lmj623565791/article/details/37992017

3.http://yifeng.studio/2016/12/15/android-fragment-attentions/