欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android中的Fragment知识点整理

程序员文章站 2022-05-13 22:41:31
...

Fragment定义

Fragment中文意思是碎片,在Android3.0以后开始引入,是一种可以嵌入在Activity当中的UI片段,能让应用充分合理利用屏幕空间;它和Activity非常像,能添加自己的布局,有自己的生命周期,甚至可以理解成一个迷你型的Activity,虽然有时候这个Fragment可能会跟普通Activity一样大。

Fragment为什么被称为第五大组件

  1. 在使用频率上,Fragment是不低于其它四大组件的
  2. Fragment拥有自己的生命周期
  3. 可以灵活的加载到Activity中

Fragment加载到Activity的两种方式

  • 静态加载:将Fragment添加到Activity的布局文件当中,作为一个xml的标签
<fragment
        android:id="@+id/fragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:name="com.mangoer.webviewwithjs.fragment.WebviewFrag"/>
  • 动态加载:使用FragmentManager管理Fragment,由FragmentTransaction进行添加替换Fragment,并由容器资源作为标志位设置Fragment所要显示在Activity中的位置,最要不能忘记使用commit方法完整整个步骤
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.add(R.id.fl_content,webviewFrag);
ft.commit();

FragmentPagerAdapter和FragmentStatePagerAdapter区别

  • FragmentPagerAdapter适用于页面较少的情况,比如主界面那种几个tab页的需求;FragmentStatePagerAdapter适用于页面较多的情况,比如新闻app类似的分类很多的tab页
  • 由于FragmentStatePagerAdapter在每次切换页卡的时候,执行到destroyItem方法的时候,是将Fragment从FragmentManager中remove掉,即销毁Fragment实例,回收内存,所以适合页面较多的需求
  • FragmentPagerAdapter在每次切换页卡的时候,执行到destroyItem方法的时候,只是调用FragmentTransaction的detach方法将Fragment视图从Activity分离,并没有回收内存,所以适合页面较少的需求,因为这样保存较少的内存对系统内存影响不大

Fragment生命周期

Android中的Fragment知识点整理

Fragment需要依附于Activity才能使用,所以它的生命周期是与Activity生命周期相关联的。
* onAttach():当Fragment创建的时候会调用onAttach方法,意思是将Fragment与Activity进行绑定,该方法有一个Activity类型参数,代表与其进行绑定的activity
* onCreate():用来创建Fragment,此时的activity还没有创建完成
* onCreateView():首次绘制Fragment的View的时候调用,从该方法返回的View必须是Fragment的根视图
* onViewCreated():表明Fragment的视图已经绘制完成,可以在这里做控件的初始化等操作, 但是,此时Fragment的视图层次结构不会附加到其父级。
* onActivityCreated():在Activity的onCreate方法执行完后回调这个方法,在这个方法里可以获取Activity的View
* onStart():当Activity调用onStart时说明Activity可见了,也说明Fragment也可见了,这时候就回调onStart
* onResume():当Activity调用onResume说明Activity可与用户交互了,这时候回调Fragment的onResume方法,说明Fragment也可与用户交互了
* onPause():Activity先调用onPause方法,然后Fragment回调,表明处于暂停状态,可见但是用户不能与之交互了
* onSaveInstanceState():保存当前Fragment的状态。该方法会自动保存Fragment的状态,比如EditText键入的文本,即使Fragment被回收又重新创建,一样能恢复EditText之前键入的文本。
* onStop():执行该方法时,Fragment完全不可见。
* onDestroyView():销毁与Fragment有关的视图,但未与Activity解除绑定,依然可以通过onCreateView方法重新创建视图
* onDestroy():销毁Fragment。通常按Back键退出或者Fragment被回收时调用此方法
* onDetach():解除与Activity的绑定

Fragment通信

Fragment与Activity通信

可以通过getActivity获取与之绑定的Activity,通过这个实例就可以调用Activity的方法;或者在Fragment创建一个接口,在Activity实现,这样也能调用接口方法,在接口方法里调用Activity方法

Activity与Fragment通信

在Activity实例化Fragment的时候保存Fragment实例,通过实例调用Fragment方法

Fragment与Fragment通信
  • 在Fragment里可以通过getActivity获取Activity,在通过Activity获取FragmentManager,再通过findFragmentById或者findFragmentByTag获取其它Fragment,这样就能与其通信
  • 第二种方法就是上面两种结合
  • 第三种方法就是使用第三方消息订阅发布框架的使用

Activity向Fragment传递数据

通常我们避免通过Fragment的构造方法往Fragment传递数据,因为在某些情况比如屏幕旋转,内存不足,Activity被重启重新调用onCreate,那Fragment也被重新实例化,这时候系统默认走的是无参构造方法,这时候就可能会出现异常了;这时候我们需要通过Bundle来保存数据,这样即使Fragment重新实例化,还是能从onCreate方法的Bundle参数中取出数据

public static FragmentTest newInstance(String value){
    FragmentTest fragment = new FragmentTest ();

    Bundle bundle = new Bundle();
    bundle.putInt("value", value);

    fragment .setArguments(bundle);
    return fragment ;
}

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Bundle bundle = getArguments();
    int args = bundle.getInt("value");
}

FragmentManager的使用

Fragment常用的三个类:

android.app.Fragment :继承这个类,定义自己的Fragment

android.app.FragmentManager: 是一个抽象类,定义了一些和 Fragment 相关的操作和内部类/接口

android.app.FragmentTransaction:开启一个事务,保证Fragment操作的原子性

  1. add(int containerViewId, Fragment fragment): 往Activity中添加一个Fragment
  2. remove(Fragment fragment):从Activity中移除一个Fragment,如果被移除的Fragment没有添加到回退栈,这个Fragment实例将会被销毁
  3. replace(int containerViewId, Fragment fragment):使用另一个Fragment替换当前的,实际上就是remove()然后add()的合体
  4. hide(Fragment fragment): 隐藏当前的Fragment,仅仅是设为不可见,并不会销毁
  5. show(Fragment fragment):显示之前隐藏的Fragment
  6. addToBackStack(String name):将Fragment添加到回退栈
  7. commit():提交上面各种操作

Fragment使用注意,踩坑

getActivity()

我们通常会在Fragment里使用该方法获取Activity引用,但在某些情况下会报NULL异常,此时getActivity返回的是空;大多数情况是因为Fragment已经onDetach所依附的Activity。
比如Fragment已经销毁,但是里面的异步任务还在执行,这时候执行getActivity就会返回一个空引用。
这时候就需要做一些保护了:

  • 比如设置标志位,Fragment销毁就停止异步任务
  • 调用getActivity的时候判断引用是否为空
  • 在Fragment的onAttach方法里保存Activity引用来替代getActivity,这有可能会造成内存泄漏,避免使用

Fragment添加重叠(api24及以上已被官方修复)

还记得Activity的onSaveInstanceState这个方法吗,当我们的Activity进入后台、屏幕旋转(没有在manifest相关设置的情况下)、跳转到其它Activity等情况下(这里只讨论android4.x以上情况,这年代还讨论低版本感觉意义不大),这个方法会被回调,然后保存activity的状态;这样当activity因为内存不足被系统杀掉后再被重启的情况下可以恢复被杀前的状态。

我们在这里也可以去保存一些数据,比如音视频播放进度、阅读类应用当前页数等,这样在应用被动重启后就可以在onCreate方法或者onRestoreInstanceState方法取出Bundle参数,获取之前数据来恢复状态。

就是因为这个机制,俗称为内存重启,重启后恢复之前展示的Fragment,而这时候如果是在onCreate里再去加载Fragment的话,就会产生重叠,这时候就需要进行如下判断

@Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        // 判空
        if(savedInstanceState == null){
              // 这里进行Fragment加载
        }
    }

这样就可以避免重复加载

这里讨论的是Activity只有一个Fragment,如果一个Activity有多个Fragment作为Tab页,那在重启的时候即使在onCreate里做了保护,也会出现多个Fragment都显示的情况,为什么呢,我们来从源码分析下:
进入FragmentActivity里看

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
        mFragments.attachHost(null /*parent*/);
        super.onCreate(savedInstanceState);
        。。。。
        if (savedInstanceState != null) {
            Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
            mFragments.restoreAllState(p, nc != null ? nc.fragments : null);
            。。。。
        }

        。。。。
}
    /**
     * Save all appropriate fragment state.
     */
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        Parcelable p = mFragments.saveAllState();
        if (p != null) {
            outState.putParcelable(FRAGMENTS_TAG, p);
        }
        。。。。
    }

可以看到,onSaveInstanceState方法保存了Fragment的状态,然后重启时在onCreate方法里恢复Fragment状态。
这里的保存和恢复都是通过mFragments这个对象来调用,其实它是FragmentController
这个类主要通过FragmentHostCallback这个类来管理Fragment生命周期及状态;

    /**
     * Saves the state for all Fragments.
     */
    public Parcelable saveAllState() {
        return mHost.mFragmentManager.saveAllState();
    }
    /**
     * Restores the saved state for all Fragments. The given Fragment list are Fragment
     * instances retained across configuration changes.
     *
     * @see #retainNonConfig()
     */
    public void restoreAllState(Parcelable state, List<Fragment> nonConfigList) {
        mHost.mFragmentManager.restoreAllState(state, nonConfigList);
    }

这里的mHost就是FragmentHostCallback,而它又是通过FragmentManagerImpl来操作,这是FragmentManager的内部类,并且是继承自FragmentManager的,去里面看看具体的方法实现

void restoreAllState(Parcelable state, List<Fragment> nonConfig) {
        //核心        
        FragmentManagerState fms = (FragmentManagerState)state;
        if (fms.mActive == null) return;

        // 重新附加那些保存了状态的未配置的Fragment状态类,这样就不用再实例化它们了
        if (nonConfig != null) {
            for (int i=0; i<nonConfig.size(); i++) {
                Fragment f = nonConfig.get(i);
                if (DEBUG) Log.v(TAG, "restoreAllState: re-attaching retained " + f);
                FragmentState fs = fms.mActive[f.mIndex];
                fs.mInstance = f;
                f.mSavedViewState = null;
                f.mBackStackNesting = 0;
                f.mInLayout = false;
                f.mAdded = false;
                f.mTarget = null;
            }
        }

                。。。
        for (int i=0; i<fms.mActive.length; i++) {

            FragmentState fs = fms.mActive[i];
            if (fs != null) {
                //通过FragmentState获取Fragment
                Fragment f = fs.instantiate(mHost, mParent);
                mActive.add(f);
                // 重新恢复Fragment后,清除FragmentState中的保存的Fragment实例,以防后续再从FragmentState恢复Fragment
                fs.mInstance = null;
            }
        }

        。。。
    }

    Parcelable saveAllState() {
        ......
        //收集所有活跃的Fragment
        int N = mActive.size();
        FragmentState[] active = new FragmentState[N];
        for (int i=0; i<N; i++) {
            Fragment f = mActive.get(i);
            if (f != null) {

                FragmentState fs = new FragmentState(f);
                active[i] = fs;
            }
        }
        ......
        //Fragment栈下标
        int[] added = null;
        BackStackState[] backStack = null;
        // 构建当前添加的Fragment列表
        if (mAdded != null) {
            N = mAdded.size();
            if (N > 0) {
                added = new int[N];
                for (int i=0; i<N; i++) {
                    added[i] = mAdded.get(i).mIndex;
                }
            }
        }
        ......
        //保存回退栈状态
        if (mBackStack != null) {
            N = mBackStack.size();
            if (N > 0) {
                backStack = new BackStackState[N];
                for (int i=0; i<N; i++) {
                    backStack[i] = new BackStackState(mBackStack.get(i));
                }
            }
        }
        FragmentManagerState fms = new FragmentManagerState();
        fms.mActive = active;
        fms.mAdded = added;
        fms.mBackStack = backStack;
        return fms;
    }

saveAllState可以看出来都是通过FragmentManagerState 这个类来完成的,比如保存活跃的Fragment,回退栈状态等

然后在restoreAllState方法,通过FragmentManagerState里的FragmentState的instantiate()方法恢复了Fragment

既然都是通过FragmentManagerState操作的,那就看下这个类,也是FragmentManager的内部类

final class FragmentManagerState implements Parcelable {
    FragmentState[] mActive;//保存了Fragment实例等的类
    int[] mAdded;//保存Fragment栈下标
    BackStackState[] mBackStack;//保存Fragment返回栈状态
    .......
}

final class FragmentState implements Parcelable {
    final String mClassName;//Fragment类名
    final int mIndex;//活跃的Fragment数组索引
    final boolean mFromLayout;//是否是从xml里添加的
    //静态添加到xml中的Fragment的View标签id或者动态添加到View层次结构中的容器id,可通过findFragmentById查找
    final int mFragmentId;
    //如果是动态添加View树中,这个就是Fragment所在的父容器id
    final int mContainerId;
    final String mTag;//我们添加的tag
    //true 如果配置更改时保留Fragment实例,比如Activity重建时该Fragment并不会销毁
    final boolean mRetainInstance;
    final boolean mDetached;
    final Bundle mArguments;

    Bundle mSavedFragmentState;

    Fragment mInstance;
    .......
    public Fragment instantiate(FragmentHostCallback host, Fragment parent) {
        if (mInstance != null) {
            return mInstance;
        }

        final Context context = host.getContext();
        if (mArguments != null) {
            mArguments.setClassLoader(context.getClassLoader());
        }

        mInstance = Fragment.instantiate(context, mClassName, mArguments);

        if (mSavedFragmentState != null) {
            mSavedFragmentState.setClassLoader(context.getClassLoader());
            mInstance.mSavedFragmentState = mSavedFragmentState;
        }
        mInstance.setIndex(mIndex, parent);
        mInstance.mFromLayout = mFromLayout;
        mInstance.mRestored = true;
        mInstance.mFragmentId = mFragmentId;
        mInstance.mContainerId = mContainerId;
        mInstance.mTag = mTag;
        mInstance.mRetainInstance = mRetainInstance;
        mInstance.mDetached = mDetached;
        mInstance.mFragmentManager = host.mFragmentManager;

        if (FragmentManagerImpl.DEBUG) Log.v(FragmentManagerImpl.TAG,
                "Instantiated fragment " + mInstance);

        return mInstance;
    }
}

这样再回头看下restoreAllState方法中就是通过FragmentState的instantiate方法获取Fragment去进行恢复的,这也表明Android系统是通过FragmentState保存Fragment的。


经过这一列源码分析,我们可以得出

  • 系统会在内存重启前帮我们保存相关Fragment的大部分内容,然后在重启后进行恢复
  • 不管保存还是恢复的时候,有效的Fragment都是通过FragmentManagerState这个类去操作
  • FragmentManagerState维护Fragment相关属性是通过FragmentState操作的

但是这样操作还是导致了Fragment重叠,为什么呢,我们可能忽略了一个重要的属性,即Fragment是否显示或隐藏,在Fragment类是这样定义的

// 当Fragment需要对用户隐藏时设置为true,默认false,即Fragment是显示的
boolean mHidden;

终极原因:回看上面代码,我们看到FragmentState中没有定义与这个对应的属性,这样当我们一个Activity有多个Fragment用来显示多个TAB时,如果发生内存重启,而Fragment的mHidden始终为false,即都为需要显示的情况下,多个Fragment就会在一个Activity中同时显示出来

注意:
我们在Activity中添加Fragment有add和replace两种方法,当中通过add方法添加多个Fragment,在重启后每个Fragment的实例存在,视图都存在,这样就会出现重叠
当使用replace添加多个Fragment时,逻辑是先销毁前一个Fragment,再添加当前Fragment,所以不存在重叠情况

解决方法:
既然知道原因了,怎么解决呢,要知道系统没有帮我们维护mHidden这个状态,那我们就需要自己来维护了

  • 在内存重启前保存Fragment显示状态
  • 重启后根据重启前的显示状态判断是否显示
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        for (int i=0; i<fragmentList.size();i++) {
            boolean isHidden = getFragmentHidden(fragmentList.get(i));
            outState.putBoolean(fragmentList.get(i).getTag(),isHidden);
        }
    }

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //这里只是举例,具体可以根据多个Fragment的TAG,获取相应显示状态,然后去设置对应的Fragment
        if (savedInstanceState != null) {
            boolean isHidden = savedInstanceState.getBoolean(TAG);
            FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
            if (isHidden) {
                ft.hide(fragment);
            } else {
                ft.show(fragment);
            }
            ft.commit();
           .......
        }
    }

上面是在Activity操作,也可以在自己定义的Fragment的父类的这两个方法去判断

题外话

上面开头说过,官方在api24及以后已经修改过这个重叠的bug了,在FragmentState里维护了mHidden这个属性,所以我们在之后的版本开发就不需要做这样的保护了;不过24以前的版本还是要乖乖做的

final class FragmentState implements Parcelable {
    final String mClassName;
    final int mIndex;
    final boolean mFromLayout;
    final int mFragmentId;
    final int mContainerId;
    final String mTag;
    final boolean mRetainInstance;
    final boolean mDetached;
    final Bundle mArguments;
    //***********************
    final boolean mHidden;

    Bundle mSavedFragmentState;

    Fragment mInstance;
}

onActivityResult问题

情景1:ActivityA中的FragmentA使用startActivityForResult跳转到ActivityB,然后在ActivityB中的FragmentB接受数据处理后,需要将数据返回到FragmentA

问题:但是Fragment没有像Activity那样提供setResult方法去设置返回数据

解决:在FragmentB中调用getActivity获取依附的Activity引用,然后调用setResult设置返回数据


情景2:假如情景1中的FragmentA里又嵌套了一个FragmentA2,然后在FragmentA2里通过startActivityForResult跳转到ActivityB中的FragmentB,然后将数据处理返回到FragmentA2

问题:通过FragmentActivity源码我们知道,在onActivityResult中只做了一次分发,即分发给了最*的Fragment,而Fragment类的onActivityResult又是一个空方法,那么*Fragment中嵌套的子Fragment将获取不到返回数据

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        mFragments.noteStateNotSaved();
        int requestIndex = requestCode>>16;
        if (requestIndex != 0) {
            requestIndex--;

            String who = mPendingFragmentActivityResults.get(requestIndex);
            mPendingFragmentActivityResults.remove(requestIndex);
            if (who == null) {
                Log.w(TAG, "Activity result delivered for unknown Fragment.");
                return;
            }
            Fragment targetFragment = mFragments.findFragmentByWho(who);
            if (targetFragment == null) {
                Log.w(TAG, "Activity result no fragment exists for who: " + who);
            } else {
                targetFragment.onActivityResult(requestCode & 0xffff, resultCode, data);
            }
            return;
        }

        super.onActivityResult(requestCode, resultCode, data);
    }

public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener {
    .........
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
    }
    ........
}

解决:需要我们自己在定义的基类Fragment中重写onActivityResult方法,进行手动分发

@Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        List<Fragment> list = getChildFragmentManager().getFragments();
        if (list != null) {
            for (Fragment f:list) {
                f.onActivityResult(requestCode, resultCode, data);
            }
        }
    }

Fragment实例化传参问题

许多对Fragment接触不深的同学喜欢这样做,通过new FragmentA(String args),即通过构造方法将参数传递给Fragment对象;这种方法在有限的情况下没有问题,但是在许多用户复杂的使用环境中会出现问题,比如屏幕旋转,应用在后台被杀等这些情况,然后又重新恢复后,之前的通过构造方法传过来的参数没了,甚至会出现crash。

这是因为Fragment在实例化的时候默认是通过无参构造方法去实例化的,所以开发者通过构造方法传进来的参数是无法保存的,看Fragment源码就可以知道了

public static Fragment instantiate(Context context, String fname, @Nullable Bundle args) {
        try {
            Class<?> clazz = sClassMap.get(fname);
            if (clazz == null) {
                // Class not found in the cache, see if it's real, and try to add it
                clazz = context.getClassLoader().loadClass(fname);
                sClassMap.put(fname, clazz);
            }
            Fragment f = (Fragment)clazz.newInstance();
            if (args != null) {
                args.setClassLoader(f.getClass().getClassLoader());
                f.mArguments = args;
            }
            return f;
        } 
    }

可以看出来Fragment是通过反射进行实例化的,并且调用的是无参构造;但是其中会通过mArguments 来保存数据,所以我们需要下面这种方法去实例化Fragment,这样我们就可以在onCreateView中获取这个Bundle拿到数据

public static FragmentA newInstance(String msg) {


        FragmentA fragment = new FragmentA ();

        Bundle args = new Bundle();

        args.putString("data", msg);


        fragment.setArguments(args);
        return fragment;

}

Fragment出栈问题

情景:一个Activity中会加载FragmentA,FragmentA中进行操作后可以由FragmentB替代展示,在FragmentB中进行操作后可以由FragmentC替代展示,以此类推,我们会通过addToBackStack将Fragment添加到回退栈,这样点击返回键就能依次返回到前一个Fragment

问题:假如某些情况下,我们在进入FragmentC前希望操作后不返回到FragmentB,是直接返回到FragmentA,那我们通过FragmentTransaction.remove()是不起作用的,它只能将未添加到返回栈的Fragment移出Fragment列表

解决:我们需要通过FragmentManager.popBackStack()方法将处于返回栈中的Fragment移出

相关方法:

//弹出回退栈中栈顶的 Fragment,异步执行的
public abstract void popBackStack();

//立即弹出回退栈中栈顶的,直接执行
public abstract boolean popBackStackImmediate();

//tag可以为null或者相对应的tag,flags只有0和1两种情况
//如果tag为null,flags为0时,弹出回退栈中最上层的那个fragment。
//如果tag为null ,flags为1时,弹出回退栈中所有fragment。
//如果tag不为null,那就会找到这个tag所对应的fragment,flags为0时,弹出该
//fragment以上的Fragment,如果是1,弹出该fragment(包括该fragment)以上的fragment。
public abstract void popBackStack(String name, int flags);

//同步操作
public abstract boolean popBackStackImmediate(String name, int flags);

//获取返回栈中Fragment个数
public abstract int getBackStackEntryCount();
//获取返回栈中指定索引的元素
public abstract BackStackEntry getBackStackEntryAt(int index);

replace使用注意

前提:没有将Fragment添加到返回栈
如果我们在Activity连续添加了多个Fragment后,再使用replace方法添加Fragment,那么前面添加的Fragment就会全部从FragmentManager列表中清除,这时候我们从FragmentManager.getFragments()方法获取Fragment,使用前需要判null。

replace 是先删除fragmentmanager中所有已添加的fragment中,容器id与当前要添加的fragment的容器id相同的fragment;然后再添加当前fragment

相关标签: Fragment