在各种Android项目中,我们不可避免要使用到Fragment,但很多地方其实我们只是习惯性或copy代码来使用,很多地方并没有深入去了解,今天就通过这篇文章整理和回顾一下关于Fragment的种种。而在自己总结的过程中,我也发现自己在很多细节方面有一些知识上的不足和理解上的错误,这也会让我对整理的知识掌握得更全面。
Fragment的生命周期
生命周期自然是首先要弄清楚的,先上我们最经常见到的Android官网关于Fragment的生命周期图:
关于Fragment最基本的生命周期大家应该都很熟悉,但有几点需要详细说一下:
1. 关于Fragment的回收
- 仔细看上图中的英文提示,会发现在Fragment的onDestroyView()方法之后有两个剪头指向,一个是直接去执行onDestroy()方法,另一个是重新去走onCreateView()方法。造成这两种情况的原因是因为FragmentManager对Fragment的不同管理方式。用一个我们最常用到的场景来说明:
ViewPager搭配Fragment来使用时,系统为我们提供了两个适用的adapter:FragmentPagerAdapter和FragmentStatePagerAdapter,这两个adapter最大的不同之处就是对Fragment的回收管理。在FragmentPagerAdapter的instantiateItem()和destroyItem方法中,对多个Fragment展示和回收的处理主要是通过FragmentTransaction的attach和detach方法来处理;而在FragmentStatePagerAdapter中,是通过通过FragmentTransaction的add和remove方法来处理。后者的Fragment在不可见时执行完onDestroyView()后直接去执行onDestroy()把当前Fragment完全销毁;而前者的Fragment在执行完onDestroyView()后则不再执行,而会在下一次这个Fragment重新可见时,去通过onCreateView()来重新创建视图,也就是说Fragment并没有被完全销毁而只是被回收了View而已。
这一点在后面关于ViewPager使用场景相关的文章中我会再在详细说到。
2. Fragment和Activity生命周期的联系
- 自然先看官方给出的联系图:
上图虽然很清晰,但Activity和Fragment生命周期每个阶段更细致的顺序并看不出来。这点只有通过手动跑一下测试代码来看了
上面是我通过一个简单的测试代码来打印的生命周期log,启动MainActivity,TestFragment显示MainActivity中。值得注意的,除了onResume方法是Activity先执行而Fragment后执行外,其他阶段的生命周期方法都是Fragment先执行之后,Activity再执行的。
3. Fragment的onSaveInstanceState方法**
onSaveInstanceState的调用时机Fragment通Activity是一样的, 都是在当前界面进入可被系统回收状态时就会被调用。看一下启动MainActivity后按home键回到桌面时,onSaveInstanceState的调用情况:
所以,当我们需要保存Fragment相关的状态时,可以通过这个方法。需要注意的一点是,Fragment本身并没有通Activity一致的onRestoreInstanceState方法,所以如官方文档所说
您可以在Fragment的onSaveInstanceState()回调期间保存状态,并可在onCreate()、onCreateView() 或 onActivityCreated() 期间恢复状态。
4. 关于Fragment的setUserVisibleHint方法**
这个方法严格来说不属于Fragment生命周期的范畴,但有人把它比作是Fragment真正的onResume和onStop方法,主要是因为配合这个方法可以在使用ViewPager+Fragment时实现懒加载,因为Fragment的onResume方法与Activity的onResume方法是一致的,所以无法通过onResume方法来判断Fragment是否可见,反而可以通过setUserVisibleHint来准确判断(关于ViewPager的懒加载后续的文章中也会详细讲到),当然如果项目中有用到友盟统计,也可以通过该方法更加准确的上报Fragment的相关数据。
说setUserVisibleHint不属于Fragment生命周期的范畴主要是因为它并不会被系统主动来回调。原先我也认为只要Fragment的可见性发生变化就会回调它,自己用代码打印时才发现并非如此。如果Activity中有一个Fragment,无论是进入另一个Activity,还是按home键回到桌面,setUserVisibleHint方法都不会调用。Google一番外加看源码,才明白原来这个方法是需要我们主动调用来告知系统当前Fragment的可见性,源码注释这样说:
/* Set a hint to the system about whether this fragment's UI is currently visible
* to the user. This hint defaults to true and is persistent across fragment instance
* state save and restore.
*
* <p>An app may set this to false to indicate that the fragment's UI is
* scrolled out of visibility or is otherwise not directly visible to the user.
* This may be used by the system to prioritize operations such as fragment lifecycle updates
* or loader ordering behavior.</p>
*
* @param isVisibleToUser true if this fragment's UI is currently visible to the user (default),
* false if it is not.
*/
public void setUserVisibleHint(boolean isVisibleToUser) {
if (!mUserVisibleHint && isVisibleToUser && mState < STARTED) {
mFragmentManager.performPendingDeferredStart(this);
}
mUserVisibleHint = isVisibleToUser;
mDeferStart = !isVisibleToUser;
}
系统为我们提供的配合ViewPager+Fragment的适配器FragmentPagerAdapter和FragmentStatePagerAdapter中,也是在ViewPager的fragment item进行初始化和切换时主动调用了该方法。
public Object instantiateItem(ViewGroup container, int position) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
final long itemId = getItemId(position);
// Do we already have this fragment?
String name = makeFragmentName(container.getId(), itemId);
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
mCurTransaction.attach(fragment);
} else {
fragment = getItem(position);
if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), itemId));
}
// 此处主动调用了setUserVisibleHint方法
if (fragment != mCurrentPrimaryItem) {
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
}
return fragment;
}
5. 其他
Activity 生命周期与Fragment生命周期之间的最显著差异在于它们在其各自返回栈中的存储方式。 默认情况下,Activity 停止时会被放入由系统管理的 Activity 返回栈(以便用户通过“返回”按钮回退到Activity,任务和返回栈对此做了阐述)。不过,仅当您在删除Fragment的事务执行期间通过调用 addToBackStack() 显式请求保存实例时,系统才会将Fragment放入由宿主 Activity 管理的返回栈。
在其他方面,管理Fragment生命周期与管理 Activity 生命周期非常相似。 因此,管理 Activity 生命周期的做法同样适用于Fragment。
注意:如需 Fragment 内的某个 Context 对象,可以调用 getActivity()。但要注意,请仅在Fragment附加到 Activity 时调用 getActivity()。如果Fragment尚未附加,或在其生命周期结束期间分离,则 getActivity() 将返回 null。
Fragment的使用
Fragment必须始终嵌套在Activity中,其生命周期直接受宿主 Activity 生命周期的影响。 例如,当 Activity 暂停时,其中的所有的Fragment也会暂停;当 Activity 被销毁时,所有的Fragment也会被销毁。 不过,当 Activity 正在运行(处于onResume状态)时,您可以独立操纵每个Fragment,如添加或移除它们。 当您执行此类Fragment事务时,您也可以将其添加到由 Activity 管理的返回栈—Activity 中的每个返回栈条目都是一条已发生Fragment事务的记录。 返回栈让用户可以通过按“返回”按钮撤消Fragment事务(后退)。
当您将Fragment作为 Activity 布局的一部分添加时,它存在于 Activity 视图层次结构的某个 ViewGroup 内部,并且Fragment会定义其自己的视图布局。您可以通过在 Activity 的布局文件中声明Fragment,将其作为 <fragment>
元素插入您的 Activity 布局中,或者通过将其添加到某个现有 ViewGroup,利用应用代码进行插入。不过,Fragment并非必须成为 Activity 布局的一部分;您还可以将没有自己 UI 的Fragment用作 Activity 的不可见工作线程。
1. 创建Fragment的视图
要想为Fragment提供布局,就必须实现onCreateView()回调方法,Android系统会在Fragment需要绘制其布局时调用该方法。对此方法的实现返回的View必须是片段布局的根视图。
要想从onCreateView()返回布局,可以通过xml中定义的布局资源来扩展布局。为此,onCreateView()专门提供了一个LayoutInflater对象。
例如,以下这个Fragment子类从 example_fragment.xml文件加载布局:
public static class ExampleFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.example_fragment, container, false);
}
}
传递至onCreateView()的container参数是你的Fragment布局将插入到的父ViewGroup(来自Activity的布局)。savedInstanceState是在恢复Fragment时,提供上一Fragment实例相关数据的Bundle。
inflate()方法带有三个参数:
- 你想要扩展的布局的资源ID;
- 将作为扩展布局父项的ViewGroup。传递 container
对系统向扩展布局的根视图(由其所属的父视图指定)应用布局参数具有重要意义; - 指示是否应该在扩展期间将扩展布局附加至 ViewGroup
(第二个参数)的布尔值。(在本例中,其值为 false,因为系统已经将扩展布局插入container
—传递 true 值会在最终布局中创建一个多余的视图组。)
关于inflate常用的三个方法的总结:
- 调用LayoutInflater.inflate方法,并且将root参数设置为null,就等于忽略了xml布局文件中的layout_×参数(而如gravity、background等这样的非layout参数则依然会生效),并返回布局文件对应的忽略layout_×参数的view对象;
- 如果root不为null的话,就根据root会为xml布局文件生成一个LayoutParam对象,如果attachToRoot参数为false,那么就将这个param对象设置给这个布局文件的View;
- 如果root不为null,并且attachRoot=true,那么就会根据root生成一个布局文件View的LayoutParam对象,并且将这个View添加到root中去,并返回这个root的View。
看inflate的源码可以看出对三种不同情况的处理:
// resource为inflate方法中传入的xml布局资源参数
final XmlResourceParser parser = res.getLayout(resource);
……
final AttributeSet attrs = Xml.asAttributeSet(parser);
……
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
……
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
2. 向Activity中添加Fragment
a. 在 Activity 的布局文件内声明片段
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<fragment
android:id="@+id/list"
android:name="com.example.news.ArticleListFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<fragment
android:id="@+id/viewer"
android:name="com.example.news.ArticleReaderFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2" />
</LinearLayout>
Fragment的android:name属性指定要在布局中实例化的Fragment。
当系统创建此 Activity 布局时,会实例化在布局中指定的每个Fragment,并为每个Fragment调用 onCreateView() 方法,以检索每个Fragment的布局。系统会直接插入Fragment返回的 View
来替代 fragment 元素。
使用这种方式时,fragment元素的id是必须设置的,否则会crash
每个Fragment都需要一个唯一的标识符,重启 Activity 时,系统可以使用该标识符来恢复Fragment(您也可以使用该标识符来捕获Fragment以执行某些事务,如将其删除)。 可以通过三种方式为Fragment提供 ID:
- 为 android:id属性提供唯一 ID
- 为 android:tag属性提供唯一字符串
- 如果您未给以上两个属性提供值,系统会使用容器视图的 ID
这里我测试发现,这样的显示Fragment时,inflate的xml中根ViewGroup的layout_×参数会被忽略掉:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@color/colorPrimary"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/colorAccent"
android:text="我是其他Fragment" />
</LinearLayout>
将上述布局的Fragment添加到一个Activity的布局中:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/second_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<fragment
android:id="@+id/otherFragment"
android:name="holenzhou.com.aboutfragment.view.OtherFragment"
android:layout_width="match_parent"
android:layout_weight="match_parent"/>
</LinearLayout>
会发现宽和高的参数被忽略掉了,而是以fragment元素的layout参数为准了。
Debug发现此时OtherFragment的onCreateView()方法中,传进来的container参数为null,很奇怪,Google了一番,发现stackoverflow上面也有类似的问答,但都没有说清楚是为什么。
b. 在代码中动态添加Fragment到某个现有的ViewGroup
您可以在 Activity 运行期间随时将Fragment添加到 Activity 布局中。您只需指定要将Fragment放入哪个 ViewGroup。
要想在您的 Activity 中执行Fragment事务(如添加、删除或替换Fragment),您必须使用 FragmentTransaction中的 API。您可以像下面这样从 Activity 获取一个 FragmentTransaction 实例:
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
然后,您可以使用 add()方法添加一个Fragment,指定要添加的Fragment以及将其插入哪个视图。例如
ExampleFragment fragment = new ExampleFragment();
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();
传递到 add() 的第一个参数是 ViewGroup ,即应该放置Fragment的位置,由资源 ID 指定,第二个参数是要添加的Fragment。
一旦您通过 FragmentTransaction 做出了更改,就必须调用 commit() 以使更改生效。
Fragment可以没有UI,用作 Activity 的不可见工作线程。添加此类型的Fragment,使用add(Fragment, String)从 Activity 添加Fragment(为Fragment提供一个唯一的字符串“标记”,而不是视图 ID)。这会添加Fragment,但由于它并不与 Activity 布局中的视图关联,因此不会收到对 onCreateView() 的调用。因此,您不需要实现该方法。
3. 管理Fragment
通过使用FragmentManager来管理Activity中的Fragment,可以执行的操作包括:
- 通过 findFragmentById()(对于在 Activity 布局中提供 UI 的Fragment)或 findFragmentByTag()(对于提供或不提供 UI 的Fragment)获取 Activity 中存在的Fragment
- 通过 popBackStack()(模拟用户发出的 Back 命令)将片段从返回栈中弹出
- 通过 addOnBackStackChangedListener() 注册一个侦听返回栈变化的侦听器
4. 执行Fragment事务
在 Activity 中使用Fragment的一大优点是,可以根据用户行为通过它们执行添加、删除、替换以及其他操作。 您提交给 Activity 的每组更改都称为事务,您可以使用 FragmentTransaction 中的 API 来执行一项事务。您也可以将每个事务保存到由 Activity 管理的返回栈内,从而让用户能够回退Fragment更改(类似于回退 Activity)。
每个事务都是您想要同时执行的一组更改。您可以使用 add(), replace(), remove() 等方法为给定事务设置您想要执行的所有更改。然后,要想将事务应用到 Activity,您必须调用 commit() 。
不过,在您调用 commit() 之前,您可能想调用 addToBackStack(),以将事务添加到Fragment事务返回栈。 该返回栈由 Activity 管理,允许用户通过按“返回”按钮返回上一Fragment状态。
例如,以下示例说明了如何将一个Fragment替换成另一个Fragment,以及如何在返回栈中保留先前状态:
// Create new fragment and transaction
Fragment newFragment = new ExampleFragment();
FragmentTransaction transaction = getFragmentManager().beginTransaction();
// Replace whatever is in the fragment_container view with this fragment,
// and add the transaction to the back stack
transaction.replace(R.id.fragment_container,newFragment);
transaction.addToBackStack(null);
// Commit the transaction
transaction.commit();
在上例中,newFragment会替换目前在 R.id.fragment_container ID 所标识的布局容器中的任何Fragment(如有)。通过调用 addToBackStack() 可将替换事务保存到返回栈,以便用户能够通过按“返回”按钮撤消事务并回退到上一Fragment。
如果您向事务添加了多个更改(如又一个 add() 或 remove()),并且调用了 addToBackStack(),则在调用 commit() 前应用的所有更改都将作为单一事务添加到返回栈,并且“返回”按钮会将它们一并撤消。
向FragmentTransaction添加更改的顺序无关紧要,不过:
- 必须最后调用 commit();
- 如果您要向同一容器添加多个Fragment,则您添加Fragment的顺序将决定它们在视图层次结构中的出现顺序。
如果您没有在执行删除Fragment的事务时调用 addToBackStack(),则事务提交时该Fragment会被销毁,用户将无法回退到该Fragment。 不过,如果您在删除Fragment时调用了 addToBackStack(),则系统会停止该Fragment,并在用户回退时将其恢复。
提示:对于每个Fragment事务,您都可以通过在提交前调用 setTransition() 来应用过渡动画。
调用 commit() 不会立即执行事务,而是在 Activity 的 UI 线程(主线程)可以执行该操作时再安排其在线程上运行。不过,如有必要,您也可以从 UI 线程调用 executePendingTransactions() 以立即执行 commit() 提交的事务。通常不必这样做,除非其他线程中的作业依赖该事务。
注意:您只能在 Activity保存其状态(用户离开 Activity)之前使用 commit() 提交事务。如果您试图在该时间点后提交,则会引发异常。 这是因为如需恢复 Activity,则提交后的状态可能会丢失。 对于丢失提交无关紧要的情况,请使用 commitAllowingStateLoss()。
5. 与Activity通信
- Fragment通过getActivity()访问Activity实例,并轻松地执行在Activity布局中查找视图等任务;
View listView = getActivity().findViewById(R.id.list);
2.Activity中通过findFragmentById()或者findFragmentByTag(),通过从FragmentManager获得对Fragment的引用来调用Fragment中的方法。例如:
ExampleFragment fragment = (ExampleFragment) getFragmentManager().findFragmentById(R.id.example_fragment);
6. 创建对 Activity 的事件回调
为了在Fragment和Activity间共享数据,可以在Fragment内定义一个回调接口。并要求宿主 Activity 实现它。 当 Activity 通过该接口收到回调时,可以根据需要与布局中的其他Fragment共享这些信息。(比如左边FragmentA中显示文章列表,右边FragmentB中显示相对应文章内容的场景)
public static class FragmentA extends ListFragment {
...
// Container Activity must implement this interface
public interface OnArticleSelectedListener {
public void onArticleSelected(Uri articleUri);
}
...
}
在onAttach()回调中强转宿主Activity为指定接口,Activity实现接口时,mListener成员会保留对 Activity 的OnArticleSelectedListener 实现的引用,以便FragmentA 可以通过调用 OnArticleSelectedListener 定义的方法与 Activity 共享事件。
public static class FragmentA extends ListFragment {
OnArticleSelectedListener mListener;
...
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mListener = (OnArticleSelectedListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString() + "must implement OnArticleSelectedListener");
}
}
...
}