Android Fragment 该怎么用?

1,207 阅读10分钟
原文链接: www.jianshu.com

这篇文章是讲有关于Fragment的使用,大部分还是比较基础的知识点。之所以写出来呢,因为我在工作中发现在使用fragment时走了很多弯路,遇见了很多坑。就是因为还有很多细节的东西没有掌握。在这里分享出来,也能方便自己回顾。

1. fragment概述

fragment是一种控制器对象,activity可以委派一些任务给它,通常这些任务是管理用户界面。受管的用户界面可以是整个屏幕或者是小部分屏幕。

fragment与支持库

Google在API11的时候引入的fragment(为满足平板UI设计的灵活性要求),最低能兼容到API4。
为了应用能够兼容旧版本设备,Android提供了开发支持库,分别是Fragment(android.support.v4.app.Fragment)、FragmentActivity(android.support.v4.app.Fragment-Activity)

关于操作系统内置版的fragment库

开发者在平时都会优先使用支持库版的fragment而不是系统内置的版本,是因为Google每年都会更新支持库,通过此来发布引入新特性,修复Bug,如果想体现这些好处,我们只需要升级项目的支持库版本就好,Google提供支持库的本意就是能够方便开发人员在不支持API的版本是能体验Fragment。

2. 托管UI fragment

我们都知道fragment是由activity来管理的,在托管fragment的期间,activity必须处理好以下俩件事:

  1. 管理好fragment的生命周期
  2. 在布局中为fragment安排位置

2.1 fragment的生命周期

fragment生命周期 fragment与Activity生命周期的对比


fragment生命周期与activity的生命周期方法非常类似,也具备停止、暂停、运行转态,也拥有可以覆盖的方法,在关键的节点时候完成一些任务,许多方法也对应了activity的生命周期方法。大部分人都知道fragment的onAttach()、onCreat()、onCreatView()这三个生命周期方法是在Activity的onCreat()的时候执行的。看过源码应该知道,这三个方法是在onCreate()中的setContentView()中调用的。

生命周期方法在开发中是非常重要的,因为fragment代表activity工作,它的状态就反应着activity的状态,显然,fragment需要相对应的生命周期方法来处理activity的工作。那么fragment与activity的生命周期方法最为关键的区别就在于,fragment的生命周期方法是由activity来调用而不是操作系统,操作系统不关心activity用来管理视图的fragment。

2.2 托管的俩种方式

activity托管fragment有以下俩种方式:

  1. 在activity布局中添加fragment
  2. 在activity通过code添加

第一种方式比较简单,直接通过activityu的布局绑定fragment,但是不够灵活,在fragment生命周期过程中,无法切换其他fragment视图,所以在此我们不多作介绍。

第二种方式:
虽然我们是要在托管的activity中通过代码添加fragment,但是首先得定义布局容器,也就是为frgament安排位置,布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
             android:id="@+id/fragment_container"
             android:layout_width="match_parent"
             android:layout_height="match_parent">

</FrameLayout>

创建fragment以及实现生命周期方法

package com.haife.album_master.activities;


import android.os.Bundle;
import android.support.v4.app.Fragment;

/**
 * A simple {@link Fragment} subclass.
 */
public class BlankFragment extends Fragment {
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

}

以上代码需要注意几点:

  • Fragment的生命周期方法都是公共方法,与Activity不同,activity生命周期方法是保护方法,前面也说了,这样是为了让托管的activity能够调用
  • 类似于activity,fragment中也有保存和获取状态的Bundle。如同activity的onSaveInstance()方法使用相同,我们也可以覆盖这个方法在fragment中
  • fragment的试图创建并不是在Fragment.onCreat()方法中生成,虽然我们在方法中配置了fragment 的实例,但创建和配置视图在另一个生命周期方法onCreatView()中完成的
package com.haife.album_master.activities;


import android.os.Bundle;
import android.support.v4.app.Fragment;

/**
 * A simple {@link Fragment} subclass.
 */
public class BlankFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        TextView textView = new TextView(getActivity());
        textView.setText(R.string.hello_blank_fragment);
        return textView;
    }
}

添加fragment到FragmentManager

FragmentManager类管理的是fragment队列和fragment的事物回退栈,下图为FragmentManager图解:

graph LR
Activity-->FragmentManager
FragmentManager-->|fragment队列|Fragment
FragmentManager-->|回退栈|FragmentTransaction

要以代码的方式添加fragment到activity中,我们只需要调用activity中的FragmentManager,首先获取FragmentManager。

 FragmentManager fm = getSupportFragmentManager();

Fragment事务

当我们在activity中获取到FragmentManager时,我们需要把fragment交由其管理。代码如下:

public class MainActivity extends FragmentActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        FragmentManager fm = getSupportFragmentManager();
        Fragment fragment = fm.findFragmentById(R.id.fragment_container);

        if (fragment == null) {
            fragment = new BlankFragment();
            fm.beginTransaction()
                    .add(R.id.fragment_container, fragment)
                    .commit();
        }
    }
}

add(...)方法是整个事务的核心,除了添加,fragment事务还可被用作移除、附加、分离、替换fragment队列中的fragment,这个是使用fragment在运行时组装和重新组装用户界面的关键,FragmentManager管理着fragment事务的回退栈。

FragmentManager.beginTransaction()方法是创建并返回FragmentTransaction实例,由此可得到一个FragmentTransaction队列。

现在我们重头到位总结一下上面的代码,首先,用过资源Id向FragmenbManager发出请求获取fragment实例,如果获取的fragment不为空,那么FragmentManager直接返回它,这里有个问题,很多人会问为什么fragment会存在于队列中呢?你明明还没有添加fragment,其实很简单,我们之前说过,当设备旋转的时候activity的生命周期会发生变化,在activity销毁的时候,activity的FragmentManager会将fragment队列保存起来,当activity执行生命周期的onCreat()方法的时候,FragmentManager会优先回去到保存的Fragment队列,然后重建重而恢复之前的状态,这样你们就知道原因了吧!下面判空的时候如果为空,就直接添加到fragment队列中0。

抽象的activity类

我们可以看到上面的实现代码几乎通用,但是唯一不足的地方就是在添加fragment到FragmentManager的时候实例化的fragment只能是BlankFragment,为了应用的灵活性。所以接下来我们继续优化。如何处理呢,那我们是不是可以在上面的activity定义一个抽象的方法来当作activity的父类,那么子类就会实现该方法来返回fragment实例。

为了区分,我们创建一个BaseFragmentActivity

public class BaseFragmentActivity extends FragmentActivity {

    protect abstrct Fragment creatFragment();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        FragmentManager fm = getSupportFragmentManager();
        Fragment fragment = fm.findFragmentById(R.id.fragment_container);

        if (fragment == null) {
            fragment = creatFragment();
            fm.beginTransaction()
                    .add(R.id.fragment_container, fragment)
                    .commit();
        }
    }
}

MainActivity修改如下

public class MainActivity extends BaseFragmentActivity {

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

    }

    @Override
    protected Fragment creatFragment(){
        retrun new BlankFragment();
    }
}

现在看起来我们的MainActivity是不是简练整洁,在实际开发中多使用抽象类会大大的节约你的开发时间,提高效率。

3.使用Fragment argument

在开发中有一个非常普遍你的需求,就是activity与fragmenr之间的值传递。
比如,现在我们需求,从一个fragment跳转到另一新的目标activity中(携带参数),然H后目标Fragment要负责接收这个值。实现代码部分如下:

目标activity

public class TargetActivity extends BaseFragmentActivity {

    public static final String EXTRA_UUID = "com.haife.album_master.activities.targetId"

    public static Intent newIntent(Context ctx,String targetId){
        Intent intent = new Intent(ctx,TargetActivity.class);
        intent.putExtra(EXTRA_UUID,targetId);
        return intent;
    }
}

跳转处

 @Override
public void onClick(){
    Intent intent = TargetActivity.newInstance(getActivity(),id);
    startActivity(intent);
}

到这里,参数id已经被被安全存储在TargetActivity中,然而获取参数和使用的是Fragment类。

fragment中获取extra信息

fragment有俩种方式来获取intent中的数据,一种比较简单直接,另一种复杂灵活的方式是涉及到fragment arguement。我们先看简单的实现方式,在fragment中通过getActivity()方式获取到TargetActivity的intent,返回至TargetFragment类,得到TargetActivity的intent 的extra信息后,在用它获取String id的值。

public class TargetFragment extends Fragment {

   public void oncreat(Bundle saveInstanceState){
       String id = getActivity().getIntent().getStringExtra(TargetAtivity.EXTRA_UUID);
   }
}

继续几行代码,我们就能从托管的activity的intent中获取到信息,然而这种方式会让我们的TargetFragment无法复用,当前只能被用于TargetActivity,如何优化?我们可以将要获取的值保存在TargetFragment的某个地方,而不是在TargetAvcivity的私有空间中,这样,fragment就不用依赖于activity的intent内指定的intent,就能获取自己所需的extra数据。这“某个地方”其实就是fragment argument。

fragment argument

每个Fragment都存在一个Bundle对象,bundle中含有键值对。我们可以类似于附加extra到activity的intent中一样,一个键值对应一个argument。如何附加arguement给fragment呢?我们需要调用Fragment.setArgument(Bundle),注意这个方法必须在fragment创建后调用。andriod开发者通用的做法是在fragment中定一个静态的newInstance()方法,在此方法内完成fragment的实例和Bundle对象的创建,然后把argument放入bundle中附加给fragment。代码如下:

package com.haife.album_master.activities;


import android.os.Bundle;
import android.support.v4.app.Fragment;

/**
 * A simple {@link Fragment} subclass.
 */
public class TargetFragment extends Fragment {
   private static final String ARG_TARGET_ID = "arg_target_id";

    public static BlankFragment newInstance(String id){
        Bundle bundle = new Bundle();
        bundle.putString(ARG_TARGET_ID,id);

        BlankFragment blankFragment = new BlankFragment();
        blankFragment.setArguments(bundle);
        return blankFragment;
    }

}

现在,需要创建fragment的时候,activity要调用BlankFragment.newInstance(String)的方法,而不是向上面那样直接调用构造方法,activity可传任意参数到newInstance()方法,按实际开发需要,这里传的是在activity中extra的值:

public class TargetActivity extends BaseFragmentActivity {

    private static final String EXTRA_UUID = "com.haife.album_master.activities.targetId"

   @Override
    protected Fragment creatFragment(){
       String id = getIntent().getStringExtra(EXTRA_UUID);
       return TargetFragment.newInstance(id);
    }
}

这里将EXTRA_ID改为私有是因为其他类都用不到了。注意的是,activity和fragment不需要也无法同时保持独立性,activity必要要了解fragment 的内部细节。

获取 argument

上面我们已经附加了argument给TargetFragment,接下来我们如何获取到他的argument?

public class TargetFragment extends Fragment {
   private static final String ARG_TARGET_ID = "arg_target_id";
    private static final String DEFAULT_VALUE = "default_value";
   @Override
   public void onCreate(Bundle saveInstanceState){
         String id = getArguments().getString(ARG_TARGET_ID, DEFAULT_VALUE);
   }

}

代码写到这里,虽然比较少,但是很通用。下面是fragment的一点小拓展,如果frament中存在一个列表项(RecycleView),当数据源发生改变时,我们该如何在fragment中刷新它?

 public class BlankFragment extends Fragment {

    @Override
    public void onResume() {
        super.onResume();
        updateUI();
    }

    private void updateUI() {
        if (mAdapter == null) {
           mAdapter = new MyAdapter(userList);
            recycle.setAdapter(mAdapter);
        }else {
            mAdapter.notifyDataSetChanger();
        }
    }


}

解释一下为什么选择覆盖onResum()方法来刷新列表,而不用onStart(),当有其他activbity位于BlankFragment的宿主activity的之前时,我们无法确定宿主activity是否会被停止,如果前面的activity是透明的,那么在onStart()方法中刷新列表是无效,一般来说,要保证fragment视图得到刷新,在onResum()方法中处理是最安全的。

到这里fragment argument就介绍完了,那为什么要使用它呢?我们为什么不直接在fragment内部创建一个实例变量?想想就知道,当操作系统重建fragment或者用户离开应用,甚至系统回收内存,又或是应用配置发生改变时,所有的实例变量就不复存在了,所以设计fragment argument的本意就是为了上述场景。当然又有人说你可以用实例状态保存下来,然后通过onSaveInstanceState(Bundle)方法存储。这当然也可以,前提是,你回过头来看代码的时候能够记的住~~~

4.通过fragment获取返回结果

fragment与activity,有Fragment.startActivityForResult(...)和Fragment.onActivityResult(...),用法类似于Activity的同名方法.但是,从fragment中返回结果的处理有些不同,fragment能从activity中接受返回结果,其自身是无法持有返回结果的。尽管Fragment有自己的startActivityForResult(...)和onActivityResult(...),却没有setResult(...)方法,相反,我们可以让activity返回结果值。具体代码如下:

public class BlankFragment extends Fragment {
    ...

    public void returnResult(){
        getActivity().setResult(Activity.Result_OK,null);
    }

    ...

5. 结尾

最后说点闲话吧,在我们设计应用时,正确的使用fragment是非常重要的,但是我们也不能滥用它。Google设计fragment的本意是封装可复用的组件,这里的组件是指屏幕的组件,单屏上最多使用2-3个fragment是比较好的。其实在选择activity还是UI fragment来管理用户界面的时候,我认为不用考虑太多。能使用fragment实现的时候,就不要去考虑activity了。