Chapter3:Fragment的生命周期及其衍生
文章目录
3.1 理解Fragment的生命周期
- 开发Android程序时,理解生命周期是至关重要的。举个例子来说,当我们将手机旋转使得应用在横屏与竖屏之间切换的时候,系统会将可见的Activity销毁然后再使用适配的资源在新的朝向上重建。如果我们的程序不能有效的与其生命周期协调,常常会导致崩溃和意料之外的响应。
- Fragment实例存在于单一的Activity之中,因此,尽管Fragment的生命周期与Activity并非完全相同,但其内部关联紧密。Fragment提供许多与Activity相似的生命周期回调方法。
- 随着我们应用的复杂度增加,以及尝试使用一些Fragment特化的类,理解Fragment的生命周期以及Fragment生命周期与Activity生命周期之间的关系是至关重要的。
3.1.1 理解Fragment的创建和展示
- 第一步,是Activity的onCreate周期。大多数情况下,Activity会在onCreate周期中调用setContentView方法,该方法会加载布局资源,并触发Activity去管理其包含的Fragments。
- 之后,Fragment在onAttach周期中,会收到附加的通知和一个指向Activity的引用。接着,Activity也会在onAttachFragment周期中,收到附加的通知和一个指向Fragment的引用。
- 在Fragment创建之前将其附加到Activity上,这一举动看起来奇怪其实却非常有用。在很多情况中,Fragment的创建过程需要访问Activity,因为Activity中常常包含Fragment需要展示或对Fragment非常重要的信息。
- 在Fragment附加到Activity之后,Fragment在onCreate周期执行一般的创建工作,之后在onCreateView周期中构建其包含的视图层级。
- 当我们的Activity包含多个Fragment时,Android会连续地调用以下四个周期:1.Fragment.onAttach,2.Activity.onAttachFragment, 3.Fragment.onCreate,
4.Fragment.onCreateView。(在一个Fragment调用完之这四个周期后再调用下一个Fragment的这四个周期,使得每一个Fragment在下一个Fragment开始前完成附加和创建的过程。)- 当所有的Fragment按照序列完成上述四个周期的调用后,会按照次序为每一个Fragment分别调用图中剩余的生命周期。
- 在Activity执行完onCreate方法的执行后,Android会调用每个Fragment的onActivityCreated周期。(Activity.onCreate周期并非在Activity.onAttachFragment周期开始前结束,Activity.onAttachFragment周期在Activity.onCreate周期执行过程中完成。)onActivityCreated周期开始表明,所有Fragment的视图(View)已经使用Activity的布局资源创建完成,可以安全的访问。
- Fragment的onStart和onResume周期,会在同名的Activity的周期开始之后开始。Fragment在onStart和onResume两个周期中的表现与Activity在这两个周期中的表现相似。
- 对于大多说的Fragment而言,我们需要重载其onCreate和onCreateView周期方法。
3.1.2 避免方法命名混淆
- Activity和Fragment有许多相同名字的回调方法,大多数这些相同名字命名的方法都具有相似的意图。但是要注意,onCreateView是一个例外。
- Android调用Fragment的onCreateView方法来创造一个时机,在这个时机完成创建Fragment所包含的视图层级,并将其返回。在Fragment的使用过程中常常重载这个方法。
- LayoutInflater类在填充布局资源时会反复调用Activity中的onCreateView方法。大多数情况下创建Activity时并不需要重载该方法。
3.1.3 理解Fragment的隐藏与销毁
- Fragment在隐藏与销毁的开始时的行为与Activity是一样的。当用户切换到另一个活动时,每个Fragment的onPause, onSaveInstanceState,onStop周期都会被调用。对于这三个生命周期的调用方法,都是Fragment的方法先被调用,然后Activity的同名方法后被调用。
- 在onStop周期被调用后,Fragment的行为开始和Activity出现差异。与Fragment视图层级创建时Fragment一个一个分别创建的机制一样,Fragment视图层级销毁时也是一个一个分别销毁的。在Activity的onStop周期被调用后,Fragment的onDestroyView周期紧接着被调用,在这之后Fragment的onDetach周期接着被调用。在这时,Fragment与Activity之间不再存在关联,getActivity方法将返回null。
- 对于一个包含多个Fragment的Activity,Android会依次为每一个Fragment调用onDestroyView, onDestroy和onDetach这三个周期方法,只有当一个Fragment执行完上述三个周期方法后,下个Fragment才会开始执行。当所有Fragment完成解绑和销毁后,开始调用Activity的onDestroy方法。
3.1.4 尽可能高效的使用资源
- 对于Fragment的生命周期的绝大部分而言,系统对其的周期管理方式和管理Activity相似。但是,要注意Fragment创建和销毁这两个阶段的存在的差异,Fragment的创建和销毁适合它们包含的视图层级分离开的,这是因为Fragment可以在缺少视图层级时存在并与Activity关联。
- 很多时候存在这样一种场景,一个Activity包含多个Fragment,但是在某一时间点只有一部分Fragment可见。在上述情况中,Activity包含的Fragments都能调用onAttach和onCreate周期方法,但是每个Fragment的onCreateView周期方法会被延迟到该Fragment在app可见时被调用。于此相似的是,当Activity包含的Fragment隐藏变为不可见时,只会调用该Fragment的onDestroyView周期方法,而onDestroy和onDetach方法并不会被调用。
- 当在Activity中动态管理Fragments时会表现出一种机制,该机制允许Fragment与Activity的关联及Fragment的初始化这两个开销只出现一次,并且同时可以轻松修改Fragment视图层级的可见性。这在我们使用FragmentTransaction类和具有Fragment管理功能的操作栏功能来明确管理Fragments的可见性时格外重要.
3.1.5 管理Fragment的状态
- 在很多Fragment的实现中,其生命周期回调方法中最重要的那个就是onSaveInstanceState方法。与Activity中的类似,这个回调方法提供了一个在Fragment被销毁前保存状态的时机。比如:当用户切换Activity或者旋转设备时,Activity和其包含的Fragments会被彻底销毁后重建。通过onSaveInstanceState方法保存的Fragment状态,会在重建调用onCreate和onCreateView 方法时在参数列表中传回。
- 当管理Fragment的状态时,你应该在创建视图层级时将一些普适的任务从特定的任务中分离出来。比如在Fragment初始化过程中普遍存在的代价高的任务 ,比如:连接数据源、复杂的运算、资源分配等,这些任务都应在onCreate方法中执行,而非在onCreateView方法中。这样的话,Fragment视图层级被销毁时Fragment仍然可以保持,可以避免对这些代价高的工作进行不必要的重复初始化。
3.2 Fragment衍生的特殊用途类
- 存在这样一些具有特定功能的类,它们本质上都是继承自Fragment,都会经历与Fragment一样的周期行为。许多这样的具有特定功能的类会对其在生命周期的各个时间点上执行何种操作是安全的产生一些影响。有些类甚至会添加自己的生命周期方法。理解这些类的功能和它们与Fragment生命周期的交互对高效地使用这些类是至关重要的。
3.2.1 ListFragment
- ListFragment是Fragment派生类中一个非常有用的特殊类。ListFragment内含一个ListView,可以便捷地展示列表数据。
3.2.1.1将列表与数据关联
- 与普通的Fragment类不同,对于ListFragment类我们不需要重写其onCreateView方法。ListFragment类提供了一个标准的使用方式,我们只需关联上数据,然后ListFragment类就会自主完成视图层级的创建和数据的展示。
- 我们通过调用ListFragment类的setListAdapter方法并传递一个实现ListAdapter接口的对象引用就可以将数据与ListFragment类关联起来。Android提供给我们一系列实现了ListAdapter接口的类,比如:ArrayAdapter, SimpleAdapter, 和SimpleCursorAdapter,具体选择使用那个类取决于你的数据的存储的方式。如果没有一种Android提供的类课余满足你的需求,你可以轻松地创建一个自定义的实现类。
- ListFragment类包裹着一个ListView类的实例,可以通过getListView方法获取。在绝大多数场景中,我们与ListView类交互并使用ListView类提供特性的时候并感觉不到ListView实例类的存在。ListFragment和ListView类都暴露一个setListAdapter方法,但是我们需要使用的是ListFragment提供的那个方法。
- 在调用ListFragment.setListAdapter 方法时,ListFragment类依赖于一些初始化行为,因此,如果绕过这些初始化行为,直接在包含ListView的实例中调用setListAdapter方法可能会使应用变得不稳定。
3.2.1.2 从显示中分离数据
先前我们展示图书信息例子中的按钮个数是固定的,这并不是一种好的选择。当我们修改图书列表时,需要直接修改Fragment的视图。实际上,我们需要的是一种动态生成RadioButton的方式。使用ListFragment类来实现会是一个轻松的方法。
对于之前的程序,我们需要将书名存在一个数组资源文件中,并将其与ListFragment实例关联起来。这样当我们需要增加新的书名或者修改现有的书名时,只需修改这个数组资源文件。
我们在values资源文件夹中创建一个名为course_arrays.xml资源文件来存储书名和内容的描述。(需要将之前array.xml中name为book_descriptions的数组删除,不可以存在重名的数字资源)
<?xml version="1.0" encoding="utf-8"?> <resources> <!-- Book Titles --> <string-array name="book_titles"> <item>@string/Book1</item> <item>@string/Book2</item> <item>@string/Book3</item> </string-array> <!-- Book Descriptions --> <string-array name="book_descriptions"> <item>欢迎阅读第一本书!</item> <item>欢迎阅读第二本书!</item> <item>欢迎阅读第三本书!</item> </string-array> </resources>
3.2.1.3 创建ListFragment派生类
注意:新版本不再使用ListView而是RecyclerView,例子将根据新版本做一定的调整。
通过Android Studio 创建Fragment(List):
首先,在BookListFragment2.java文件指定mColumnCount参数。这里我们设置为1:
/** * 如果mColumnCount不大于1,将采用线性布局(LinearLayout) * 如果mColumnCount大于1,将采用网格布局(GridLayout) */ private int mColumnCount = 1;
然后,在onCreate周期回调中将书名的数组资源导入:
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //数据导入 bookTitles = new ArrayList<>(); Collections.addAll(bookTitles, getResources().getStringArray(R.array.book_titles)); }
Android Studio生成的BookListFragment2类中包含了一个内嵌的接口OnFragmentInteractionListener。我们在例子中创建过一个OnSelectedBookChangeListener的接口,这个接口做的工作就是这个自动生成接口要做的工作,我们可以将OnFragmentInteractionListener全部替换为OnSelectedBookChangeListener。
修改完的BookListFragment2.java类:
public class BookListFragment2 extends Fragment { private int mColumnCount = 1; private List<String> bookTitles; private OnSelectedBookChangeListener mListener; public BookListFragment2() { } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //数据导入 bookTitles = new ArrayList<>(); Collections.addAll(bookTitles, getResources().getStringArray(R.array.book_titles)); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_string_list, container, false); // 设置adapter if (view instanceof RecyclerView) { Context context = view.getContext(); RecyclerView recyclerView = (RecyclerView) view; if (mColumnCount <= 1) { recyclerView.setLayoutManager(new LinearLayoutManager(context)); } else { recyclerView.setLayoutManager(new GridLayoutManager(context, mColumnCount)); } recyclerView.setAdapter(new BookTitlesViewAdapter(bookTitles, mListener)); } return view; } @Override public void onAttach(Context context) { super.onAttach(context); if (context instanceof OnSelectedBookChangeListener) { mListener = (OnSelectedBookChangeListener) context; } else { throw new RuntimeException(context.toString() + " must implement OnSelectedBookChangeListener"); } } @Override public void onDetach() { super.onDetach(); mListener = null; } }
**BookTitlesViewAdapter的实现:
public class BookTitlesViewAdapter extends RecyclerView.Adapter<BookTitlesViewAdapter.ViewHolder> { private final List<String> mValues; private final OnSelectedBookChangeListener mListener; public BookTitlesViewAdapter(List<String> items, OnSelectedBookChangeListener listener) { mValues = items; mListener = listener; } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.fragment_title, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(final ViewHolder holder, final int position) { holder.mItem = mValues.get(position); holder.mContentView.setText(mValues.get(position)); holder.mView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (null != mListener) { mListener.onSelectedBookChanged(position); } } }); } @Override public int getItemCount() { return mValues.size(); } public class ViewHolder extends RecyclerView.ViewHolder { public final View mView; public final TextView mContentView; public String mItem; public ViewHolder(View view) { super(view); mView = view; mContentView = view.findViewById(R.id.content); } } }
fragment_title.xml的内容:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:id="@+id/content" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="8dp" android:textSize="16sp" /> </LinearLayout>
最后,我们将使用BookListFragment资源的位置,替换为BookListFragment2即可。(位于activity_main.xml和activity_main_wide.xml中)
最终效果:
3.3.2 DialogFragment
- 对于以前的Dialog而言,它的实现概念是在应用程序上的另一个window,使用这种Dialog的难度在于:我们必须做很多工作来处理那些Dialog中与其他UI界面中的差异化,比如:当我们处理按钮点击事件时,在Dialog中要实现DialogInterface.OnClickListener这一特定接口,而非我们平常实现按钮点击事件监听的View.OnClickListener 接口。还有一个更加复杂的问题:调转设备的朝向。当设备方向切换时,Dialog会被自动关闭,这样当一个用户在Dialog界面转换设备朝向时会得到一个与先前不一致的app行为,这会带来糟糕的体验感。
- 使用DialogFragment类会减少很多那些在使用Dialog时需专门处理的工作(比如:上述提及的点击事件监听接口)。并且在我们使用DialogFragment类时,展示与管理它时会与处理其他平常的界面时行为更加一致(比如:当设备方向切换时,DialogFragment不会自动关闭)。
3.3.2.1 风格样式
当我们的应用展示DialogFragment类的实例时,窗口可被分为三部分:布局区域(layout area)、标题(title)、边框(frame)。一个DialogFragment类的实例往往包含布局区域,但是我们可以通过setStyle方法来控制它是否包含标题和边框。DialogFragment类提供四种风格样式:
Style Has title Has frame Accepts input STYLE_NORMAL Yes Yes Yes STYLE_NO_TITLE No Yes Yes STYLE_NO_FRAM No No Yes STYLE_NO_INPUT No No No 当我们不调用setStyle方法时,会默认使用STYLE_NORMAL。
DialogFragment类的风格样式可能会影响该类的其余行为,因此需要在onCreate回调方法中设置其风格样式。在这之后的生命周期再去尝试设置DialogFragment类风格样式会被忽略。
如果你想给DialogFragment指定一个主题,可以将主题的资源Id传入setStyle方法。如果希望Android系统基于样式选择适当的主题,只需传递0作为主题的资源Id。举例:
class MyDialogFragment extends DialogFragment { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setStyle(DialogFragment.STYLE_NO_TITLE, 0); } }
3.3.2.2 布局(Layout )
DialogFragment类中设置布局与标准的Fragment派生类一样。我们只需要简单的重载onCreateView方法并且将布局资源填充进去即可:
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View theView = inflater.inflate(R.layout.fragment_my_dialog, container, false); return theView; }
创建DialogFragment的布局资源也与创建标准的Fragment派生类的布局资源一样。举例:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <!-- Text --> <TextView android:layout_width="fill_parent" android:layout_height="0px" android:layout_margin="16dp" android:layout_weight="1" android:text="这是一个DialogFragment!" /> <!-- Two buttons side-by-side --> <LinearLayout android:layout_width="fill_parent" android:layout_height="0px" android:layout_weight="3" android:orientation="horizontal"> <Button android:id="@+id/btnYes" android:layout_width="0px" android:layout_height="wrap_content" android:layout_margin="16dp" android:layout_weight="1" android:text="YES" /> <Button android:id="@+id/btnNo" android:layout_width="0px" android:layout_height="wrap_content" android:layout_margin="16dp" android:layout_weight="1" android:text="No" /> </LinearLayout> </LinearLayout>
3.3.2.3 展示DialogFragment
展示DialogFragment主要分为两部分:创建类实例和调用show方法。虽然DialogFragment实例在展示时和Dialog一样,但它实际上是一个Fragment。与所有的Fragment一样,DialogFragment是由包含它的Activity的FragmentManager来管理的。因此,我们需要传递Activity的FragmentManager的引用,作为调用show方法的一部分:
MyDialogFragment theDialog = new MyDialogFragment(); theDialog.show(getSupportFragmentManager(),null);
注意:新版下调用的是getSupportFragmentManager方法来获取FragmentManager。
显示效果:
3.3.2.4 DialogFragment中的事件处理
DialogFragment类相比传统的Dialog类具有更好的一致性。使用DialogFragment类时与使用其他Fragment时是大致相同的。比如:1.监听点击事件时使用标准的接口;2.当设备朝向改变时,不需要再做特殊的任务。
当我们在DialogFragment类中处理点击事件时,只需要简单的实现View.OnClickListener接口即可。举例:
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View theView = inflater.inflate(R.layout.fragment_my_dialog, container, false); // 设置Yes按钮的监听事件,并将焦点聚焦在Yes按键上 View yesButton = theView.findViewById(R.id.btnYes); yesButton.setOnClickListener(this); yesButton.requestFocus(); // 设置No按钮的监听事件 View noButton = theView.findViewById(R.id.btnNo); noButton.setOnClickListener(this); return theView; }
我们处理与DialogFragment类交互时通知Activity的方式也与一般的Fragment派生类一致。就像我们之前选择书目标题通知Activity的例子一样,我们的DialogFragment类也可以简单的提供一个接口来通知Activity我们点击了那个按钮:
public class MyDialogFragment extends DialogFragment implements View.OnClickListener { // 接收通知的Activity类需要实现的接口 public interface OnButtonClickListener { void onButtonClick(int buttonId); } ... }
和之前例子相似,我们可以将包含我们DialogFragment类的Activity向下转型为我们期望的接口类型。比如:
public void onClick(View view) { int buttonId = view.getId(); // 将点击行为通知给Activity OnButtonClickListener parentActivity = (OnButtonClickListener)getActivity(); parentActivity.onButtonClick(buttonId); //关闭DialogFragment dismiss(); }
注意:当我们不再希望我们的DialogFragment类展示的时候,必须调用dismiss方法。
3.2.2.5 Dialog标识(identity)
- 虽然我们将DialogFragment类当做Fragment来操作,但是它的一部分标识仍然和传统的Dialog绑定在一起。实际上,Android系统会将我们的DialogFragment类包裹在一个传统的Dialog实例中。因为这种特殊性的存在,在使用DialogFragment类时,会在onCreateView周期回调函数被调用之前,调用一个名为onCreateDialog的周期回调函数。
- onCreateDialog方法返回的Dialog实例就是最终展示给用户的窗口。我们在DialogFragment类中填充的布局最终会被包裹在Dialog的窗口中。我们可以在执行关联Dialog类的生命周期后获取到这个Dialog的实例。
3.2.2.6 与Dialog相关的访问行为
要在DialogFragment类中使用与Dialog相关的方法,就需要获取到在onCreateDialog周期中创建的Dialog实例的引用,我们可以通过getDialog方法取回这个引用。当我们拿到Dialog实例的引用时,我们就可以使用Dialog标识下的特有方法。
当我们创建一个风格为STYLE_NORMAL的DialogFragment类时,会在展示的Dialog布局区域(layout area)上包含一个标题区域(title area)。这个标题的值只能通过包含该DialogFragment类的Dialog实例的setTitle方法设置。在对话框取消的行为上存在一个与传统Dialog相似的问题:在用户点击对话框外的空白区时会关闭掉对话框,这在我们需要用户做出一个选择时,是非常不友好的。代码举例:
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View theView = inflater.inflate(R.layout.fragment_my_dialog, container, false); View yesButton = theView.findViewById(R.id.btnYes); yesButton.setOnClickListener(this); yesButton.requestFocus(); View noButton = theView.findViewById(R.id.btnNo); noButton.setOnClickListener(this); // 设置DialogFragment的Dialog层面的行为 Dialog dialog = getDialog(); dialog.setTitle(getString(R.string.myDialogFragmentTitle)); dialog.setCanceledOnTouchOutside(false); return theView; }
这段代码首先设置了对话框的标题,然后设置了防止用户通过单击对话框外空白使对话框窗口关闭。要使setTitle方法的调用生效,我们需要更改在onCreate回调方法中调用的setStyle方法,将样式设置为STYLE_NORMAL,使得对话框将有一个标题区域。
注意:如果标题显示不出来,可能是主题的问题,可以将setStyle方法调整为:
setStyle(DialogFragment.STYLE_NORMAL, android.R.style.Theme_Material_Light_Dialog_Alert);
3.2.2.7 将已存在的Dialog包裹在Fragment中
可能会存在这样一种情况:我们希望我们的程序可以像DialogFragment类一样有良好的一致性,又希望能充分利用那些继承自传统Dialog类的性质。通过重载DialogFragment类的onCreateDialog方法,我们就可以满足上述的需求。
重载onCreateDialog方法使我们可以替换掉DialogFragment类默认生成的Dialog实例。使用Android的AlertDialog类就是一个很好的例子。
AlertDialog类提供多种默认功能,允许我们不需要创建布局资源就可以展示文字、图标、按钮。这里我们需要记住AlertDialog类是一个继承自传统Dialog类的类。虽然使用AlertDialog的DialogFragment的派生类从外部的交互来看与其他的DialogFragment派生类具有相同的一致性,但是它所具有的与Dialog相关的交互功能都是通过传统Dialog类的方式实现的。举例而言,创建一个利用AlertDialog类的DialogFragment派生类需要我们通过Dialog类的方式实现点击事件的控制,也就是说要去实现DialogInterface.OnClickListener接口。
public class AlertDialogFragment extends DialogFragment implements DialogInterface.OnClickListener{ }
当我们想去展示AlertDialog是历史,我们要在onCreateDialog周期回调方法中使用AlertDialog.Builder类创建AlertDialog实例。在onCreateDialog周期回调方法中,我们将通过AlertDialog.Builder实例设置所有选项,包括标题、消息、图标、按键。这里要注意的是我们从不调用AlertDialog.Builder 类的show方法,我们要调用它的create方法。然后,我们会从新创建的AlertDialog实例中获得一个引用,并将这个引用在onCreateDialog方法中返回。
public Dialog onCreateDialog(Bundle savedInstanceState) { // 创建AlertDialog的Builder AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); // 设置AlertDialog的选项 builder.setTitle(R.string.alert_dialog_title) .setMessage(R.string.alert_dialog_message) .setIcon(R.drawable.ic_launcher) .setCancelable(false) .setPositiveButton(R.string.text_yes, this) .setNegativeButton(R.string.text_no, this); // 创建AlertDialog并返回其引用 AlertDialog alertDialog = builder.create(); return alertDialog; }
重载DialogFragment类的onCreateDialog周期回调方法是一个有利的方式,这使得我们可以在享受DialogFragment类带来的优点的同时也能使用传统Dialog中存在的功能。
3.3 参考资料
- CreatingDynamicUIwithAndroidFragments,2ndEdition