Android Fragment的一些使用细节

来源:互联网 发布:阿里云 李津 编辑:程序博客网 时间:2024/06/05 01:09

作为Android基本组件之一,介绍Android Fragment基本用法的文章已经非常多了,但是Fagment在使用时有很多的细节需要注意。文本主要列举Fragment在使用时需要注意的各种细节。

定义Fragment类

两类Fragment

Fragment是从Android 3.0(API Level 11) 才开始有的,为了兼容Android 3.0之前的系统,Android在v4包中又包含了一个Fragment的实现。

这两类Fragment在xml中使用时都用<fragment>标签,不需要区分。但是这两类Fragment在代码中不能通用。Android SDK中的Fragment完整类名为android.app.Fragment,v4包中的Fragment完整类名为android.support.v4.app.Fragment。继承自android.support.v4.app.Fragment的类的对象,不能将其赋值给android.app.Fragment变量,反之亦然。

此外,在管理两类Fragment时也需要使用不同的FragmentManager,不能混用。Android SDK中的Fragment使用Android SDK中的FragmentManager,对应的完整类名为android.app.FragmentManager,在Activity中通过getFragmentManager()来获取此对象。v4包中的Fragment使用v4包中的FragmentManager,对应的完整类名为android.support.v4.app.FragmentManager,在Activity中通过getSupportFragmentManager()来获取此对象。

要使用v4中的Fragment,则Activity需要继承自v4中的FragmentActivity(android.support.v4.app.FragmentActivity),不能直接继承自Activity(android.app.Activity)。只有FragmentActivity中才有getSupportFragmentManager()方法。此外,在FragmentActivity中同样可以使用android.app.Fragment,也就是说FragmentActivity可以同时支持两类Fragment,而普通Activity则只支持android.app.Fragment,不支持android.support.v4.app.Fragment。但无论是普通Activity还是FragmentActivity,都只有在Android 3.0之后才支持android.app.Fragment。

定义Fragment类

定义Fragment类只需要继承自上述两类Fragment中的一种即可。如果APP不需要兼容Android 3.0之前的系统,就使用Android SDK中的Fragment, 如果要兼容,则可以使用v4包的Fragment。

Fragment的构造方法

一般在定义的Fragment类中都会重写其生命周期的一些方法,在其中实现需要的功能。对Fragment的构造方法则很少实现,但是Fragment是可以有构造方法的。

如果要在XML中使用Fragment,则Fragment类必须有一个无参的构造方法,如果尝试在XML中使用一个没有无参构造方法的Fragment,则会抛出如下异常。

这里写图片描述

如果只在代码中使用,则可以使用其有参的构造方法。例如有如下Fragment定义。

public class MainFragment extends android.support.v4.app.Fragment {    public int mCount;    public MainFragment(int count) {        mCount = count;    }}

则可以在代码中通过new来创建Fragment对象并使用。

MainFragment fragment = new MainFragment(5);getSupportFragmentManager().beginTransaction().add(R.id.fragment1, fragment).commit();

虽然看起来上述代码可以正常编译运行,但这里存在一个隐患。

由于Activity可能会在系统配置发生改变或内存不足时被系统杀死,而在这之后Activity可能会被重新创建。当Activity被重新创建时,它所包含的Fragment也会被重新创建。这时系统会通过Fragment的无参构造方法来创建Fragment。所以,如果Fragment没有无参构造方法,则Fragment重建时就会产生异常。因此,无论如何,Fragment类都需要有无参的构造方法。此外,由于系统重建Fragment时不会调用其有参的构造方法,所以之前通过代码创建Fragment时使用的参数就会丢失。这样重建后的Fragment也许就不再和之前的一样的。因此,为Fragment定义有参的构造方法是不推荐的。当在Fragment类中定义有参的构造方法时,在Android Studio中会有如下的一段提示信息。

这里写图片描述

大意就是,每个Fragment类都需要有一个无参的构造方法。如果要向Fragment传递参数,可以通过setArguments()方法来设置,不要通过构造方法参数,或其他形式来传递。

XML中使用Fragment

<fragment>标签

在XML中使用Fragment需要用<fragment>标签。两类Fragment在xml中使用时都用<fragment>标签,不需要区分。示例如下。

<fragment    android:id="@+id/fragment1"    android:name="cnx.ccpat.testapp.MainFragment"    android:layout_width="wrap_content"    android:layout_height="wrap_content"/>

这里的MainFragment既可以是继承自android.app.Fragment的Fragment类,也可以继承自android.support.v4.app.Fragment的Fragment类。

<fragment>标签首字母小写问题

注意到<fragment>标签中的f是小写的,而在XML布局文件中更常见的首字母大写的各种标签,例如<RelativeLayout>,<TextView>等。之所以这些标签首字母大写,是因为它们都指定了某个具体的View类。而这里的fragment并非指定某个具体的Fragment类(指定具体的Fragment类是通过其android:name属性指定的),它是类似于<include>,<merge>这样的表示某种功能性的占位符,所以<fragment>用小写的f。

参考: http://stackoverflow.com/questions/21948034/why-is-the-fragment-element-in-android-layout-written-with-a-lower-case-f

<fragment>标签的android:name属性

在XML中使用<fragment>必须使用其android:name属性来指定加载的Fragment类名,如果不指定,则会抛出NullPointerException。如图所示。

这里写图片描述

这里需要使用完整的类名,指定的类必须存在,如果指定的类不存在,则会抛出ClassNotFoundException异常。如图所示。

这里写图片描述

指定的类必须是android.app.Fragment或android.support.v4.app.Fragment的子类,但不能是android.app.Fragment或android.support.v4.app.Fragment本身。如果是android.app.Fragment或android.support.v4.app.Fragment本身,则会抛出IllegalStateException,如图所示。

这里写图片描述

这是因为,在FragmentManagerImpl类中调用Fragment的onCreateView()之后会检查Fragment的mView对象是否为null,如果是null就会抛出此异常。而Fragment的mView对象正是Fragment的onCreateView()方法返回的,所以如果Fragment的onCreateView()方法返回了null,就会出现这个异常。android.app.Fragment,android.support.v4.app.Fragment,android.app.DialogFragment,android.support.v4.app.DialogFragment的onCreateView()方法都返回null,所以它们都不能直接在XML中使用。但是它们都是可以在代码中创建,并可以通过FragmentManager添加到布局上,如下调用是合法的。不过这显然没有任何意义。

Fragment fragment = new Fragment();getSupportFragmentManager().beginTransaction().replace(R.id.fragment1, fragment).commit();

<fragment>标签的android:id属性

<fragment>标签的android:id属性用来标记一个Fragment。在代码中可以通过id来找到对应的Fragment对象,对其进行操作。和一般的View的id不同的是,并不能用加载布局之后的View或Activity的findViewById()方法来获取Fragment对象,而是需要通过FragmentManager的findFragmentById()方法来得到Fragment对象。

例如,在XML中定义了一个Fragment,其id为fragment1,则在代码中可以通过如下方式获取这个Fragment对象。注意,要根据Fragment类型选择使用getFragmentManager()还是getSupportFragmentManager(),如果使用了错误的FragmentManager类型,即使id正确,也无法得到正确的Fragment对象(findFragmentById()会返回null)。

// 如果这个Fragment是继承自android.app.FragmentgetFragmentManager().findFragmentById(R.id.fragment1);// 如果这个Fragment是继承自android.support.v4.app.FragmentgetSupportFragmentManager().findFragmentById(R.id.fragment1);

一般来说,在XML中定义<fragment>时都需要为其添加android:id属性。

<fragment>标签的android:tag属性

和android:id属性一样,android:tag属性也可以用来标记一个Fragment。在代码中可以通过FragmentManager的findFragmentByTag()方法来得到Fragment对象。

例如,在XML中定义了一个Fragment,其tag为fragment1,则在代码中可以通过如下方式获取这个Fragment对象。

// 如果这个Fragment是继承自android.app.FragmentgetFragmentManager().findFragmentByTag("tag");// 如果这个Fragment是继承自android.support.v4.app.FragmentgetSupportFragmentManager().findFragmentByTag("tag");

在<fragment>标签中可以同时定义android:id和android:tag属性,则在代码中既可以用findFragmentById()也可以用findFragmentByTag()来获取这个Fragment对象。

<fragment>标签的tools:layout属性

<fragment>标签的tools:layout属性用来标记该Fragment所对应的布局文件,Android Studio会根据此属性在预览时加载对应的布局文件。此属性仅对Android Studio预览时有效,不影响APP的编译和运行。

代码中使用Fragment

生成Fragment对象

要创建一个新的Fragment对象,直接new出来即可。

添加Fragment到布局中

如果要添加Fragment对象到布局中,可以使用FragmentManager的add()方法。FragmentManager有几个重载的add()方法,包含三个参数的add()方法如下。

public FragmentTransaction add(int containerViewId, Fragment fragment, String tag);

这里的三个参数分别说明如下。


containerViewId

containerViewId表示要将Fragment添加到的容器对象,它可以省略,如果省略containerViewId,则表示添加到当前布局中id为0的Layout中。省略containerViewId则必须包含tag。

这里的containerViewId必须对应当前布局中某个已经存在对象,如果当前布局中没有找到containerViewId对应的对象,则会抛出IllegalArgumentException异常。如图所示。

这里写图片描述

containerViewId对应的对象可以是Fragment,也可以是View。

containerViewId对应的对象如果是View的话,该View必须继承自ViewGroup,例如FrameLayout,LinearLayout等。执行add()后,会将新添加的Fragment对应的View作为一个child添加到该ViewGroup中。如果containerViewId对应的View没有继承自ViewGroup,例如它是个TextView,则会抛出ClassCastException异常。如图所示。

这里写图片描述

containerViewId对应的对象如果是Fragment,这时情况会略复杂。首先,containerViewId一定对应XML中某个<fragment>标签的id,如前所述,XML中的Fragment一定是包含View的(其mView成员变量不能是null),而这个View是在Fragment的onCreateView()方法中作为返回值返回得到的,在Fragment的onCreateView()方法通常都是加载另一个XML来得到View对象并返回的,而XML的根标签一定是某个Layout,所以这个View通常也是继承自ViewGroup的。这时执行add()后,会将新添加的Fragment对应的View作为一个child添加到这个ViewGroup中。但是这只是一般情况,事实上,Fragment执行onCreateView()时可以不加载布局文件,直接new一个View来使用也是允许的。如果这个直接new出来的View没有继承自ViewGroup,则执行add()时同样会抛出ClassCastException异常。

举例说明,假设FirstFragment的onCreateView()实现如下,它返回的是一个new出来的TextView。

@Overridepublic View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {    return new TextView(getActivity());}

在XML中有如下布局

<fragment    android:id="@+id/fragment1"    android:name="cnx.ccpat.testapp.FirstFragment"    android:layout_width="wrap_content"    android:layout_height="wrap_content"/>

假设我们要添加一个SecondFragment到fragment1中。执行如下代码。

Fragment fragment = new SecondFragment();getSupportFragmentManager().beginTransaction().add(R.id.fragment1, fragment).commit();

这时就会抛出如下异常。

这里写图片描述

由此可见,无论containerViewId对应的对象如果是Fragment还是View,最终都是将它当做ViewGroup来使用,区别仅在Fragment是将它的mView成员变量作为ViewGroup来使用。

containerViewId除了用来表示添加fragment的容器外,还会被作为新添加的fragment的id。在执行完add()后,就可以通过FragmentManager的findFragmentById(containerViewId)来找到此Fragment。


fragment

fragment是要添加的Fragment对象。

fragment不能是null,否者会抛出NullPointerException,如图所示。

这里写图片描述

已经添加过的Fragment只有在remove之后才可以重新添加,否者会抛出IllegalStateException。如图所示。

这里写图片描述


tag

tag是为该Fragment设置的一个名字,之后可以通过FragmentManager的findFragmentByTag()来查找这个Fragment对象。tag可以省略,如果省略tag,则Fragment没有名字,只能通过id来查找。如果省略tag,则containerViewId不能省略。


介绍完add()方法的三个参数后,这里再总结下通过add()添加一个Fragment之后产生的变化。

  • 添加的fragment会被赋予id和tag属性(如果指定了tag参数的话)
  • 添加的fragment会被加入到FragmentManager的管理中,在之后可以通过FragmentManager对其进行查找,移除等操作
  • 添加的fragment对应的View(也就是其mView成员变量)会被添加到当前布局中

Fragment和Activity的交互

Activity中查找Fragment对象

如前所示,对定义在XML中的Fragment,可以通过FragmentManager的findFragmentById()或者findFragmentByTag()来获取,对通过代码添加到布局的Fragment同样也可以用这两个方法来获取。

查找Fragment的关键就是Fragment的id和tag。无论是在XML中定义的Fragment还是通过代码添加的Fragment,Fragment的id和tag都是它被加入到FragmentManager管理之后才被赋予的属性,这点可以从Fragment的方法可以看到,Fragment类只有getId()和getTag()方法,并没有setId()和setTag()方法。一个刚刚创建出来没有被add的Fragment是没有id和tag属性的。对一个已经add的Fragment,如果它被remove了,但remove操作没有被添加到回退栈中,就意味着它不再被FragmentManager管理,也就失去了id和tag。当然,如果remove操作被添加到了回退栈中,被remove的Fragment还是会保留id和tag的。

由于Fragment的id和tag都是它被加入到FragmentManager管理之后才被赋予的属性,所以讨论一个孤立的Fragment的id和tag是没有意义的(孤立的Fragment的id始终为0,tag为null)。这里的查找Fragment也只针对那些FragmentManager可以管理的Fragment,对孤立的Fragment只能通过变量引用的方式使用,不需要再查找。后文讲到Fragment查找时都不包含孤立的Fragment。

id和tag都不具有唯一性,同一个id或tag可能同时被多个Fragment所使用。如果多个Fragment公用一个id或tag,在查找Fragment时,会返回其中最近被add的那个Fragment。

例如,如下代码add了10个Fragment,每个Fragment的id都是一样的(R.id.fragment1),但tag不同(从0到9)。

for (int i = 0; i < 10; i++) {    Fragment fragment = new SecondFragment();    getSupportFragmentManager().beginTransaction().add(R.id.fragment1, fragment, String.valueOf(i)).commit();}

之后,如果执行getSupportFragmentManager().findFragmentById(R.id.fragment1),则会得到最近添加的那个Fragment,也就是tag为9的那个Fragment。

Fragment中获取Activity对象

在Fragment中可以通过getActivity()方法获取到当前Fragment所依附的Activity对象,获取到Activity对象后,就可以调用Activity中相应的方法。但是使用getActivity()时需要十分小心,因为getActivity()只有在Fragment被添加到Activity中的时候才能返回对应的Activity对象,否则会返回null。所以在异步事件中使用getActivity()的时候一般需要加上是否为null的判断,以免引起NullPointerException。

有些文章建议在Fragment中保存Activity对象的引用。如下代码在onAttach()时为mActivity赋值,在onDetach()时清空mActivity。但是这样做其实并没有什么意义,mActivity和getActivity()的返回结果是一样的,完全可以用getActivity()替代mActivity。如果去掉onDetach()中对mActivity = null;的操作,则会造成Activity对象的泄露,而且也不应该在Fragment从Activity中detach之后,还去调用原先Activity中的方法。

private Activity mActivity;@Overridepublic void onAttach(Activity activity) {    super.onAttach(activity);    mActivity = activity;}@Overridepublic void onDetach() {    super.onDetach();    mActivity = null;}

Fragment中返回Activity结果

我们知道可以在Activity中通过startActivityForResult()方法来启动一个Activity,在新启动的Activity中执行完需要的计算后,通过setResult()来返回结果。然而有些时候我们需要在Fragment中完成计算并返回结果,但是Fragment类中并没有setResult()方法,要想在Fragment中返回结果,只能通过调用它所依附的Activity对象的setResult()方法。除非确信Fragment此时没有detach,调用setResult()时一定要先判断Activity对象是否是null。如下所示。

if (getActivity() != null) {    getActivity().setResult(Activity.RESULT_OK);}

Fragment中启动Activity

不同于setResult(),Fragment中有startActivity()和startActivityForResult()方法,在Fragment中是可以直接调用startActivity()或startActivityForResult()方法来启动一个Activity的。需要注意的是,在调用startActivity()或startActivityForResult()之前一定要确保Fragment是attach到Activity上的,否则会造成异常。

如下代码在创建Intent时使用了当前Fragment所依附的Activity对象作为参数,如果getActivity()返回的是null,则new Intent(getActivity(), SecondActivity.class)会产生NullPointerException。

Intent intent = new Intent(getActivity(), SecondActivity.class);startActivity(intent);

正确的做法是

if (getActivity() != null) {    Intent intent = new Intent(getActivity(), SecondActivity.class);    startActivity(intent);                }

如下代码虽然没有用到getActivity(),但如果此时Fragment已经从Activity中detach了,startActivity(intent)时仍然会产生异常,只是这时变成了IllegalStateException。

Intent intent = new Intent();String packageName = "cnx.ccpat.testapp";String className = "cnx.ccpat.testapp.SecondActivity";intent.setClassName(packageName, className);startActivityForResult(intent, 5);

这里写图片描述

正确的做法是

if (getActivity() != null) {    Intent intent = new Intent();    String packageName = "cnx.ccpat.testapp";    String className = "cnx.ccpat.testapp.SecondActivity";    intent.setClassName(packageName, className);    startActivityForResult(intent, 5);            }

要在Fragment中启动一个Activity,除了可以直接在Fragment中调用startActivity()或startActivityForResult()方法之外,还可以在Fragment中调用s它依附的Activity对象的startActivity()或startActivityForResult()方法。如下所示。

if (getActivity() != null) {    Intent intent = new Intent(getActivity(), SecondActivity.class);    getActivity().startActivity(intent);                }

可以看到,它和上述直接启动Activity的区别仅仅是startActivity(intent)换成了getActivity().startActivity(intent)。这两种方式都能够启动Activity,但是它们之间仍然有区别,这点会在下文中介绍。

Fragment中获取Activity结果

当在Fragment中通过startActivityForResult()方法启动一个Activity后,意味着我们希望在Fragment中能够接收到Activity执行的结果。在Fragment中有onActivityResult()方法可以用来接收这个结果,只需要重写这个方法即可。

@Overridepublic void onActivityResult(int requestCode, int resultCode, Intent data) {    super.onActivityResult(requestCode, resultCode, data);    doSomething();}

这里需要注意的是,当从启动的Activity返回后,原先Fragment的onActivityResult()方法并不一定能够被Android框架所自动执行。要想Fragment的onActivityResult()方法被自动执行,需要满足一定的条件。

  1. Fragment没有从Activity中detach
  2. 如果重写了Fragment所依附的Activity对象的onActivityResult()方法,在Activity对象的onActivityResult()方法中一定要调用super.onActivityResult(),否则Fragment中的onActivityResult()不会被自动执行。
  3. 在Fragment中调用startActivityForResult()时,一定要调用自身的startActivityForResult(),而不能用getActivity().startActivityForResult(),如果用getActivity().startActivityForResult(),则Fragment的onActivityResult()方法不会被自动执行。

这是因为,在Fragment中启动了一个Activity,无论是直接调用startActivityForResult(),还是调用getActivity().startActivityForResult(),这个Activity返回的结果一定是先传递给Fragment所依附的Activity,而不会直接传递给Fragment。在FragmentActivity的onActivityResult()中会从当前所有依附的Fragment中查找requestCode对应的Fragment,找到后才会调用Fragment的onActivityResult()。所以,如果原先的Fragment已经detach,则FragmentActivity的onActivityResult()在查找时不会找到这个Fragment,也就不会调用Fragment的onActivityResult()。如果重写了Activity的onActivityResult(),但没有调用super.onActivityResult(),则FragmentActivity的onActivityResult()不会被调用,Fragment的onActivityResult()也就不会被自动调用。如果在Fragment中调用的是getActivity().startActivityForResult()时,FragmentActivity的onActivityResult()在查找时同样找不到requestCode对应的Fragment(只有在Fragment中直接调用startActivityForResult(),Activity才会记录requestCode和Fragment的对应关系),于是Fragment的onActivityResult()无法被自动调用。

在实际开发时,存在一种常见的场景:在Fragment中通过getActivity().startActivityForResult()启动了一个Activity,同时又需要在Fragment的onActivityResult()接收返回结果。例如,在Fragment中调用了某第三方SDK中的API,这个API以Activity作为参数,对这个API而言,并不知道Fragment的存在,它启动Activity也是通过Activity的startActivityForResult()方法。如前所述,Fragment中通过其依附的Activity对象的startActivityForResult()方法启动Activity后,Fragment的onActivityResult()是无法被自动调用的,要想Fragment中接收Activity结果,只能重写其依附Activity的onActivityResult(),然后手动调用Fragment的onActivityResult()。

@Overrideprotected void onActivityResult(int requestCode, int resultCode, Intent data) {    super.onActivityResult(requestCode, resultCode, data);    Fragment fragment1 = getSupportFragmentManager().findFragmentById(R.id.layout1);    fragment1.onActivityResult(requestCode, resultCode, data);}
0 0
原创粉丝点击